diff --git a/.gitignore b/.gitignore index 4cda1c1d..228f5459 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ vendor/ bin/ -composer.lock \ No newline at end of file +composer.lock +.vagrant +/.php_cs.cache diff --git a/.php_cs b/.php_cs new file mode 100644 index 00000000..3eaf3dae --- /dev/null +++ b/.php_cs @@ -0,0 +1,38 @@ +in([ + __DIR__.'/src', + __DIR__.'/tests', + ]) + ->notPath('/fixtures/') +; + +return \PhpCsFixer\Config::create() + ->setRules([ + '@Symfony' => true, + '@Symfony:risky' => true, + '@PHP56Migration' => true, + '@PHP56Migration:risky' => true, + '@PHP70Migration' => true, + '@PHP70Migration:risky' => true, + '@PHP71Migration' => true, + '@PHP71Migration:risky' => true, + 'array_syntax' => [ + 'syntax' => 'short' + ], + 'combine_consecutive_unsets' => true, + 'declare_strict_types' => true, + 'linebreak_after_opening_tag' => true, + 'modernize_types_casting' => true, + 'native_function_invocation' => true, + 'no_php4_constructor' => true, + 'ordered_imports' => true, + 'php_unit_strict' => true, + 'phpdoc_order' => true, + 'strict_comparison' => true, + 'strict_param' => true, + ]) + ->setRiskyAllowed(true) + ->setFinder($finder) +; diff --git a/.scrutinizer.yml b/.scrutinizer.yml new file mode 100644 index 00000000..60d211bf --- /dev/null +++ b/.scrutinizer.yml @@ -0,0 +1,11 @@ +filter: + paths: [src/*] +checks: + php: + code_rating: true + duplication: true +tools: + external_code_coverage: true + php_code_sniffer: + config: + standard: "PSR2" diff --git a/.travis.yml b/.travis.yml index 9431df92..41708d6f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,36 +1,68 @@ language: php +sudo: required -php: - - 5.4 - - 5.5 - - 5.6 -# - hhvm +services: + - docker + +cache: + directories: + - $HOME/.composer/cache + +php: 7.2 env: + global: + - TEST_COMMAND="composer test" matrix: - - SYMFONY_VERSION="2.3.*" - - SYMFONY_VERSION="2.6.*" + - DOCKER_API_VERSION=1.36 DOCKER_VERSION=18.02.0~ce-0~ubuntu + - DOCKER_API_VERSION=1.36 DOCKER_VERSION=18.01.0~ce-0~ubuntu + - DOCKER_API_VERSION=1.35 DOCKER_VERSION=17.12.0~ce-0~ubuntu + - DOCKER_API_VERSION=1.34 DOCKER_VERSION=17.11.0~ce-0~ubuntu + - DOCKER_API_VERSION=1.33 DOCKER_VERSION=17.10.0~ce-0~ubuntu + - DOCKER_API_VERSION=1.32 DOCKER_VERSION=17.09.0~ce-0~ubuntu + - DOCKER_API_VERSION=1.31 DOCKER_VERSION=17.07.0~ce-0~ubuntu + - DOCKER_API_VERSION=1.30 DOCKER_VERSION=17.06.2~ce-0~ubuntu + - DOCKER_API_VERSION=1.29 DOCKER_VERSION=17.05.0~ce-0~ubuntu-trusty + - DOCKER_API_VERSION=1.28 DOCKER_VERSION=17.04.0~ce-0~ubuntu-trusty + - DOCKER_API_VERSION=1.27 DOCKER_VERSION=17.03.2~ce-0~ubuntu-trusty + - DOCKER_API_VERSION=1.26 DOCKER_VERSION=17.03.2~ce-0~ubuntu-trusty + - DOCKER_API_VERSION=1.25 DOCKER_VERSION=17.03.2~ce-0~ubuntu-trusty matrix: allow_failures: - php: hhvm + fast_finish: true + include: + - php: 7.1 + sudo: required + services: + - docker + env: DOCKER_API_VERSION=1.25 DOCKER_VERSION=17.03.2~ce-0~ubuntu-trusty COMPOSER_FLAGS="--prefer-stable --prefer-lowest" COVERAGE=true TEST_COMMAND="composer test-ci" + - php: 7.1 + env: DOCKER_API_VERSION=1.36 DOCKER_VERSION=18.02.0~ce-0~ubuntu TEST_COMMAND="composer lint" + - php: 7.2 + sudo: required + services: + - docker + env: DOCKER_API_VERSION=1.36 DOCKER_VERSION=18.02.0~ce-0~ubuntu + +before_install: + - sudo apt-get update + - sudo apt-cache madison docker-ce + - sudo apt-get -o Dpkg::Options::="--force-confnew" install -y --force-yes docker-ce=${DOCKER_VERSION} + - travis_retry composer self-update install: - - echo exit 101 | sudo tee /usr/sbin/policy-rc.d - - sudo chmod +x /usr/sbin/policy-rc.d - - sudo apt-get update -qq - - sudo apt-get install -y slirp lxc aufs-tools cgroup-lite - - sudo mkdir -p /var/lib/docker - - curl -sLo lxc-docker_amd64.deb http://get.docker.io/ubuntu/pool/main/l/lxc-docker-1.4.1/lxc-docker-1.4.1_1.4.1_amd64.deb -# The Docker 1.5.0 deb package seems faulty..let's use 1.4.1 until there is a fix -# - curl -sLo lxc-docker_amd64.deb http://get.docker.io/ubuntu/pool/main/l/lxc-docker-1.5.0/lxc-docker-1.5.0_1.5.0_amd64.deb - - sudo dpkg -i lxc-docker_amd64.deb - - curl -sLo linux https://github.com/jpetazzo/sekexe/raw/master/uml - - chmod +x linux - - composer require --no-update symfony/filesystem:${SYMFONY_VERSION} - - composer require --no-update symfony/process:${SYMFONY_VERSION} - - COMPOSER_ROOT_VERSION=dev-master composer --prefer-source install + - travis_retry composer install + - travis_retry composer update ${COMPOSER_FLAGS} --no-interaction + - travis_retry composer require docker-php/docker-php-api:4.${DOCKER_API_VERSION}.* script: - - sudo ./linux quiet mem=2G rootfstype=hostfs rw eth0=slirp,,/usr/bin/slirp-fullbolt init=$(pwd)/docker.sh WORKDIR=$(pwd) HOME=$(pwd) PATH=$PATH - - bash -c "exit $(cat /tmp/build.status)" + - $TEST_COMMAND + +after_success: + - if [[ "$COVERAGE" = true ]]; then wget https://scrutinizer-ci.com/ocular.phar; fi + - if [[ "$COVERAGE" = true ]]; then php ocular.phar code-coverage:upload --format=php-clover build/coverage.xml; fi + +after_script: + - sudo cat /var/log/upstart/docker.log diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..b3b5716d --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,14 @@ +# Changelog + +## 2.0 + + - [BC Break] All endpoints have new names and potentially new parameters + - Add async (with amp artax) support + - It now uses the official swagger specification of docker + - Allow to use from 1.25 to 1.36 api version of Docker + - Add support for more keywords in ContextBuilder + - Lot of bug fixes + +## 1.24.0 - 08/08/2014 + + - [BC Break] Listing containers now return `ContainerInfo` object (instead of `ContainerConfig`) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..6be589b9 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,94 @@ +# Contributing + +If you're here, you would like to contribute to this repository and you're really welcome! + + +## Bug reports + +If you find a bug or a documentation issue, please report it or even better: fix it :). If you report it, +please be as precise as possible. Here is a little list of required information: + + - Precise description of the bug + - Details of your environment (for example: OS, PHP version, installed extensions) + - Backtrace which might help identifying the bug + + +## Feature requests + +If you think a feature is missing, please report it or even better: implement it :). If you report it, describe the more +precisely what you would like to see implemented and we will discuss what is the best approach for it. If you can do +some research before submitting it and link the resources to your description, you're awesome! It will allow us to more +easily understood/implement it. + + +## Sending a Pull Request + +If you're here, you are going to fix a bug or implement a feature and you're the best! To do it, first fork the repository, clone it and create a new branch with the following commands: + +``` bash +$ git clone git@github.com:your-name/docker-php.git +$ git checkout -b feature-or-bug-fix-description +``` + +Then install the dependencies through [Composer](https://getcomposer.org/): + +``` bash +$ composer install +``` + +Write code and tests. When you are ready, run the tests. (This is usually [PHPUnit](http://phpunit.de/) or [PHPSpec](http://phpspec.net/)) + +``` bash +$ composer test +``` + +When you are ready with the code, tested it and documented it, you can commit and push it with the following commands: + +``` bash +$ git commit -m "Feature or bug fix description" +$ git push origin feature-or-bug-fix-description +``` + +**Note:** Please write your commit messages in the imperative and follow the [guidelines](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html) for clear and concise messages. + +Then [create a pull request](https://help.github.com/articles/creating-a-pull-request/) on GitHub. + +Please make sure that each individual commit in your pull request is meaningful. If you had to make multiple intermediate commits while developing, please squash them before submitting with the following commands (here, we assume you would like to squash 3 commits in a single one): + +``` bash +$ git rebase -i HEAD~3 +``` + +If your branch conflicts with the master branch, you will need to rebase and repush it with the following commands: + +``` bash +$ git remote add upstream git@github.com:docker-php/docker-php.git +$ git pull --rebase upstream master +$ git push -f origin feature-or-bug-fix-description +``` + +## Internal + +This library consist, for the most part, of auto generated code where the reference is an [Open API Specification (Swagger v2)](https://openapis.org/). In order to modify API +endpoint or requested / returned object, you will need to update the `docker-swagger.json` file instead of files in the `generated` directory. + +There is a bash script at the root of this repository `generate.sh` which helps launching the command to generate files according to the +specification. + +When changing the specification don't hesitate to do 2 commits for better reading: + + * One with only changes to the specification + * One with changes on the generated code + +Having this also helps backporting changes to the specification in previous versions. + + +## Coding standard + +This repository follows the [PSR-2 standard](http://www.php-fig.org/psr/psr-2/) and so, if you want to contribute, +you must follow these rules. + + +## Semver + +We are trying to follow [semver](http://semver.org/). When you are making BC breaking changes, please let us know why you think it is important. In this case, your patch can only be included in the next major version. diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..b5b4ca44 --- /dev/null +++ b/LICENSE @@ -0,0 +1,20 @@ +Copyright (c) 2013 Geoffrey Bachelet and +contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/README.md b/README.md index e1add4d0..27464be6 100644 --- a/README.md +++ b/README.md @@ -1,39 +1,42 @@ -Docker PHP -========== - -**Docker PHP** (for lack of a better name) is a [Docker](http://docker.com/) client written in PHP. This library is still a work in progress. Not much is supported yet, but the goal is to reach 100% API support. +# No longer maintained -The test suite currently passes against the [Docker Remote API v1.17](http://docs.docker.com/reference/api/docker_remote_api_v1.17/). - -[![Documentation Status](https://readthedocs.org/projects/docker-php/badge/?version=latest)](http://docker-php.readthedocs.org/en/latest/) [![Travis-CI](https://travis-ci.org/stage1/docker-php.svg?branch=master)](https://travis-ci.org/stage1/docker-php) [![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/stage1/docker-php/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/stage1/docker-php/?branch=master) - -Versioning ----------- +I'm backing off maintaining this library due to a lack of motivation, time and usage of docker, contact me on twitter https://twitter.com/joelwurtz if you wish to take over this repository (or just do a fork). -There is no *stable* version yet and the API is rapidly evolving, but we still try to semantically version the library according to [semver](http://semver.org/), but shifted a little bit: +Docker PHP +========== -* **MAJOR** version number stays to 0 until API freeze -* **MINOR** version number is incremented when a backward incompatible change is made -* **PATCH** version number is incremented when a new feature is added +**Docker PHP** (for lack of a better name) is a [Docker](http://docker.com/) client written in PHP. +This library aim to reach 100% API support of the Docker Engine. -So basically, if you want the `0.5` version set, use a version constraint of `~0.5.0` and you should be fine. +The test suite currently passes against Docker Remote API v1.25 to v1.36. -We are **NOT** documenting upgrade procedures until we reach a stable API, please read the code and PRs to keep up with what's going on. You can also ask us for help, we're nice people! +[![Documentation Status](https://readthedocs.org/projects/docker-php/badge/?version=latest)](http://docker-php.readthedocs.org/en/latest/) +[![Latest Version](https://img.shields.io/github/release/docker-php/docker-php.svg?style=flat-square)](https://github.com/docker-php/docker-php/releases) +[![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square)](LICENSE) +[![Build Status](https://img.shields.io/travis/docker-php/docker-php.svg?branch=master&style=flat-square)](https://travis-ci.org/docker-php/docker-php) +[![Code Coverage](https://img.shields.io/scrutinizer/coverage/g/docker-php/docker-php.svg?style=flat-square)](https://scrutinizer-ci.com/g/docker-php/docker-php) +[![Quality Score](https://img.shields.io/scrutinizer/g/docker-php/docker-php.svg?style=flat-square)](https://scrutinizer-ci.com/g/docker-php/docker-php) +[![Total Downloads](https://img.shields.io/packagist/dt/docker-php/docker-php.svg?style=flat-square)](https://packagist.org/packages/docker-php/docker-php) +[![#docker-php on Slack](http://slack.httplug.io/badge.svg)](http://slack.httplug.io) Installation ------------ The recommended way to install Docker PHP is of course to use [Composer](http://getcomposer.org/): -```json -{ - "require": { - "stage1/docker-php": "@dev" - } -} +```bash +composer require docker-php/docker-php ``` -**Note**: there is no stable version of Docker PHP yet. +Docker API Version +------------------ + +By default it will use the last version of docker api available, if you want to fix a version (like 1.25) you can add this +requirement to composer: + +```bash +composer require "docker-php/docker-php-api:4.1.25.*" +``` Usage ----- @@ -43,7 +46,7 @@ See [the documentation](http://docker-php.readthedocs.org/en/latest/). Unit Tests ---------- -Setup the test suite using [Composer](http://getcomposer.org/): +Setup the test suite using [Composer](http://getcomposer.org/) if not already done: ``` $ composer install --dev @@ -52,60 +55,20 @@ $ composer install --dev Run it using [PHPUnit](http://phpunit.de/): ``` -$ bin/phpunit +$ composer test ``` Contributing ------------ -Here are a few rules to follow in order to ease code reviews, and discussions before maintainers accept and merge your work. - -* You **MUST** follow the [PSR-1](http://www.php-fig.org/psr/1/) and [PSR-2](http://www.php-fig.org/psr/2/). -* You **MUST** run the test suite. -* You **MUST** write (or update) unit tests. -* You **SHOULD** write documentation. - -Please, write [commit messages that make sense](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html), and [rebase your branch](http://git-scm.com/book/en/Git-Branching-Rebasing) before submitting your Pull Request. - -One may ask you to [squash your commits](http://gitready.com/advanced/2009/02/10/squashing-commits-with-rebase.html) too. This is used to "clean" your Pull Request before merging it (we don't want commits such as `fix tests`, `fix 2`, `fix 3`, etc.). - -Also, when creating your Pull Request on GitHub, you **MUST** write a description which gives the context and/or explains why you are creating it. - -Thank you! +Please see [CONTRIBUTING](CONTRIBUTING.md) for details. Credits ------- This README heavily inspired by [willdurand/Negotiation](https://github.com/willdurand/Negotiation) by @willdurand. This guy is pretty awesome. -Projects --------- - -Projects known to be using docker-php: - -* [JoliCi](https://github.com/jolicode/JoliCi), Run your tests on different and isolated stacks - License ------- -The MIT License (MIT) - -Copyright (c) 2013 Geoffrey Bachelet - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. +The MIT License (MIT). Please see [License File](LICENSE) for more information. diff --git a/composer.json b/composer.json index 02ebe1e7..64019814 100644 --- a/composer.json +++ b/composer.json @@ -1,25 +1,53 @@ { - "name": "stage1/docker-php", + "name": "docker-php/docker-php", "license": "MIT", "type": "library", "description": "A Docker PHP client", "autoload": { - "psr-0": { - "": "src/" + "psr-4": { + "Docker\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "Docker\\Tests\\": "tests/" } }, "require": { - "php": ">=5.4", - "symfony/filesystem": "~2.3", - "symfony/process": "~2.3", - "guzzlehttp/guzzle": "~4.1", - "guzzlehttp/streams": "~1.3" + "php": ">=7.1", + "docker-php/docker-php-api": "4.1.*", + "guzzlehttp/psr7": "^1.2", + "php-http/client-common": "^1.6", + "php-http/socket-client": "^1.3", + "php-http/message": "^1.0", + "symfony/filesystem": "^2.3 || ^3.0 || ^4.0", + "symfony/process": "^2.3 || ^3.0 || ^4.0" + }, + "suggest": { + "php-http/httplug-bundle": "For integration with Symfony", + "amphp/artax": "To use the async api" }, "require-dev": { - "phpunit/phpunit": "~3.7" + "phpunit/phpunit": "^6.0", + "friendsofphp/php-cs-fixer": "2.8.1", + "amphp/artax": "^3.0", + "amphp/socket": "^0.10.5" }, - "config": { - "bin-dir": "bin" + "conflict": { + "amphp/socket": "<0.10.5", + "amphp/artax": "<3.0" + }, + "scripts": { + "test": "vendor/bin/phpunit", + "test-ci": "vendor/bin/phpunit --coverage-clover build/coverage.xml", + "lint": "vendor/bin/php-cs-fixer fix --dry-run --verbose --diff", + "lint-fix": "vendor/bin/php-cs-fixer fix --verbose" + }, + "extra": { + "branch-alias": { + "dev-master": "2.x-dev" + } }, - "minimum-stability": "stable" + "prefer-stable": true, + "minimum-stability": "dev" } diff --git a/docker.sh b/docker.sh deleted file mode 100755 index 285f5c47..00000000 --- a/docker.sh +++ /dev/null @@ -1,62 +0,0 @@ -#!/bin/bash - -# Exit on first error -set -e - -save_and_shutdown() { - # save built for host result - echo $? > /tmp/build.status - # force clean shutdown - halt -f -} - -# make sure we shut down cleanly -trap save_and_shutdown EXIT SIGINT SIGTERM - -# go back to where we were invoked -cd $WORKDIR - -# can't do much without proc! -mount -t proc none /proc - -# pseudo-terminal devices -mkdir -p /dev/pts -mount -t devpts none /dev/pts - -# shared memory a good idea -mkdir -p /dev/shm -mount -t tmpfs none /dev/shm - -# sysfs a good idea -mount -t sysfs none /sys - -# pidfiles and such like -mkdir -p /var/run -mount -t tmpfs none /var/run - -# takes the pain out of cgroups -cgroups-mount - -# mount /var/lib/docker with a tmpfs -mount -t tmpfs none /var/lib/docker - -# enable ipv4 forwarding for docker -echo 1 > /proc/sys/net/ipv4/ip_forward - -# configure networking -ip addr add 127.0.0.1 dev lo -ip link set lo up -ip addr add 10.1.1.1/24 dev eth0 -ip link set eth0 up -ip route add default via 10.1.1.254 - -# configure dns (google public) -mkdir -p /run/resolvconf -echo 'nameserver 8.8.8.8' > /run/resolvconf/resolv.conf -mount --bind /run/resolvconf/resolv.conf /etc/resolv.conf - -# Start docker daemon -docker -d -H 0.0.0.0:4243 -H unix:///var/run/docker.sock 2>> /dev/null >> /dev/null & -sleep 5 - -DOCKER_HOST=tcp://127.0.0.1:4243 php -d default_socket_timeout=5 bin/phpunit -c phpunit.xml.dist diff --git a/docs/async.md b/docs/async.md new file mode 100644 index 00000000..2411e9dc --- /dev/null +++ b/docs/async.md @@ -0,0 +1,52 @@ +# Asynchronous Client + +Starting from 2.0, Docker-PHP proposes an Asynchronous PHP Client using [Amp](https://amphp.org/) and +[Artax](https://amphp.org/artax/). + +## Installation + +Since it's optional you will have to require artax with composer to use it: + +``` +composer require amphp/artax:^3.0 +``` + +## Usage + +Then you can use the `DockerAsync` API Client: + +```php +setImage('busybox:latest'); + $containerConfig->setCmd(['echo', '-n', 'output']); + $containerConfig->setAttachStdout(true); + $containerConfig->setLabels(new \ArrayObject(['docker-php-test' => 'true'])); + + $response = yield $docker->imageCreate(null, [ + 'fromImage' => 'busybox:latest', + ], DockerAsync::FETCH_RESPONSE); + + yield $response->getBody(); + + $containerCreate = yield $docker->containerCreate($containerConfig); + $containerStart = yield $docker->containerStart($containerCreate->getId()); + /** @var \Docker\API\Model\ContainersIdJsonGetResponse200 $containerInfo */ + $containerInfo = yield $docker->containerInspect($containerCreate->getId()); + + var_dump($containerInfo->getName()); +}); +``` + +API of `DockerAsync` is exactly the same as `Docker`, at the exception that each endpoint will always return an `Amp\Promise` +object, which allows to you to do parallelism and await them with the `yield` keyword. + +If you are not familiar with this kind of API, please look at the [Amp documentation on that subject](https://amphp.org/getting-started/) diff --git a/docs/basic.md b/docs/basic.md index 211d0c75..52a7f200 100644 --- a/docs/basic.md +++ b/docs/basic.md @@ -1,97 +1,19 @@ -Once `docker-php` is installed through composer you can start using it: +# Basic Usage -## Connecting +`Docker\Docker` API Client offers all endpoints available for your version of Docker. Each endpoint has a strong PHPDoc +documentation in its comment, so the best way to know what values to set for an endpoint and what it returns is to go +directly to the endpoint documentation in the code. -By default, Docker-PHP does not make any assumption on where your docker daemon is, you will need to specify the entrypoint when creating the client. - -By default, Docker-PHP uses the `DOCKER_HOST` environment variable to connect to a running `dockerd`, if not set it will use `unix:///var/run/docker.sock`. -You can, however, connect to an arbitrary server by passing an instance of the transport entrypoint `Docker\Http\Client`: +As an example for listing container you can do: ```php comb.pem -cat cert.pem >> comb.pem -cat ca.pem >> comb.pem -``` - - -```yml -parameters: - docker_entrypoint: 'tcp://192.168.59.103:2376' - docker_cert_path: '/Users/username/.boot2docker/certs/boot2docker-vm' - -services: - docker.client: - class: Docker\Http\DockerClient - arguments: - - [] - - %docker_entrypoint% - - - ssl: - local_cert: '%docker_cert_path%/comb.pem' - - true - docker: - class: Docker\Docker - arguments: - - @docker.client - -``` - - -Docker-PHP is now available as `docker`. If you extend the default Controller from the Symfony FrameworkBundle you can write: - -```php use Docker\Docker; -class ... -{ - public function indexAction() - { - /** @var Docker $docker */ - $docker = $this->get('docker'); - - $imageManager = $docker->getImageManager(); - $containerManager = $docker->getContainerManager(); - } -} +$docker = Docker::create(); +$containers = $docker->containerList(); -``` \ No newline at end of file +foreach ($containers as $container) { + var_dump($container->getNames()); +} +``` diff --git a/docs/connection.md b/docs/connection.md new file mode 100644 index 00000000..92c15212 --- /dev/null +++ b/docs/connection.md @@ -0,0 +1,74 @@ +# Connecting to Docker + +## Default, with environment variables + +By default, Docker-PHP uses the the same environment variables as the Docker command line to connect to a running `docker daemon`: + + * `DOCKER_HOST`: tcp address for the docker daemon (i.e. tcp://127.0.0.1:2376) + * `DOCKER_TLS_VERIFY`: if set to true use tls for authentication of the client + * `DOCKER_CERT_PATH`: path for the client certificates to use for authentication + * `DOCKER_PEER_NAME`: peer name of the docker daemon (as set in the certificate) + +If the `DOCKER_HOST` environment variable is not set, it will use `unix:///var/run/docker.sock` as the default tcp address. + +```php + 'tcp://127.0.0.1:2375', + 'ssl' => false, +]); +$docker = Docker::create($client); +``` + +Since `DockerClientFactory` will create a `Http\Client\Socket\Client`, you can go on the +[official documentation of the socket client](http://docs.php-http.org/en/latest/clients/socket-client.html) +to learn about possible options. + +## Custom client + +In fact `Docker\Docker` accepts any client from [Httplug](http://httplug.io/) (respecting the `Http\Client\HttpClient` interface). + +So you can use [React](https://github.com/reactphp/http-client), [Guzzle](http://docs.guzzlephp.org/en/latest/) +or any [other adapters / clients](http://docs.php-http.org/en/latest/clients.html). + + +```php + 'ubuntu:precise']); -$docker->getContainerManager()->run($container); -``` - -The `run()` method provides a few options to handle the workflow of your container. - -### Streaming a running container's output - -You can automatically attach a created container and read its output by using a callback. -The callback function receives two arguments: the type of stream, and a piece of content. The type can be either `0`, `1` or `2`, denoting respectively `stdin`, `stdout` and `stderr` (as per [Docker's Attach multiplexing protocol](http://docs.docker.io/en/latest/reference/api/docker_remote_api_v1.9/#attach-to-a-container)). - -```php -getContainerManager(); -$manager->run($container, function($output, $type) { - fputs($type === 1 ? STDOUT : STDERR, $output); -}); -``` -### Running a container as a daemon - -You can run a container as a daemon (effectively making the `run()` method non-blocking) by passing `true` as its fourth argument. - -```php -run($container, $callback, [], true); -``` - -## Creating, starting, attaching and waiting for containers - -The `run()` command is actually a composite of `create()`, `start()`, `attach` and `wait`, just like the `docker run` CLI command that you might be used to. You can use these methods to gain more fine-grained control over your containers' workflow. - -It's important to attach before starting the container, otherwise you may miss some commands output. - -```php - 'ubuntu:precise']); - -$manager = $docker->getContainerManager(); -$manager->create($container); - -printf('Created container with Id "%s"', $container->getId()); - -$manager->attach($container)->getBody()->readWithCallBack(function($output, $type) { - print($output); -}); - -$manager->start($container); -$manager->wait($container); -``` - -The `attach()` method can also retrieve logs from a stopped container. - -```php -attach($container, true)->getBody()->__toString(); -``` - -### Mapping a container's ports - -You can map a container's private ports to the host's public ports using the `Docker\PortCollection` class. - -```php -setExposedPorts($ports); - -$manager - ->create($container) - ->start($container, ['PortBindings' => $ports->toSpec()]); -``` - -The `PortCollection` class understands the complete "Docker format" for specifying ports, which looks like this: `[[hostIp:][hostPort]:]port[/protocol]`. For example, all the following definitions are valid: - -``` - host ip | host port | container port | protocol - -0.0.0.0:2222:22/tcp -> 0.0.0.0 | 2222 | 22 | tcp -127.0.0.1::22 -> 127.0.0.1 | 22 | 22 | tcp -5678/udp -> | 5678 | 5678 | udp -``` - -Once the container is running, you can retrieve the mapped ports using the `getMappedPorts()` and `getMappedPort()` methods. - -```php -start($container, $hostConfig); - -$sshPort = $container->getMappedPort(22); -printf('SSH port is mapped to %d', $sshPort->getHostPort()); - -foreach ($container->getMappedPorts() as $mappedPort) { - printf('Container\'s %d port is mapped to %d', $port->getPort(), $port->getHostPort()); -} -``` - - -## Finding and inspecting Containers - -You can either find all containers with the `findAll()` methods or lookup one particular container by its id using `find()`. - -```php -getContainerManager(); - -foreach ($manager->findAll() as $container) { - // $container is an instance of Docker\Container -} - -$id = retrieve_some_container_id_somehow(); -$container = $manager->find($id); -``` - -If you just have the id of a container, you can use the `inspect()` method to retrieve more informations about it, like its exit code if relevant. - -```php -$container = new Container(); -$container->setId($someContainerId); - -$manager->inspect($container); - -printf('Container "%s" exited with code "%d"', $container->getId(), $container->getExitCode()); -``` - -## Stopping and removing containers - -You can stop and remove containers using the `stop()` and `remove()` methods respectively. - -```php -stop($container) - ->remove($container); -``` - -The `remove` method has a second argument (defaults to `false`) `$volumes` which allows you to remove the volumes associated with the container by setting it to `true`. - - -```php -$manager->remove($container, true); -``` - - -## Removing multiple containers - -You can remove multiple containers at once by passing an array of `Docker\Container` instances or strings (container id or container name) to the `removeContainers()` method. - -```php -$manager->removeContainers([$container, '889ceddbb88e', 'angry_goodall']); -``` - -Same as the `remove` method it has a second argument (defaults to `false`) `$volumes` which allows you to remove the volumes associated with the containers by setting it to `true`. - -Keep in mind that all of the containers have to be stopped before they can be removed. - - -## The Docker\Container class - -The `Docker\Container` class is designed to help you manipulate containers. It has a few helper methods to set common runtime options. - -```php -setImage('ubuntu:precise'); -$container->setMemory(1024*1024*128); -$container->setEnv(['SYMFONY_ENV=prod', 'FOO=bar']); -$container->setCmd(['/bin/echo', 'Hello Docker!']); - -$manager->run($container); - -printf('Container\'s id is %s', $container->getId()); -printf('Container\'s name is $s', $container->getName()); -printf('Container\'s exit code is %d', $container->getExitCode()); -``` - -### Configuring exposed ports - -Use the `Docker\PortCollection` class to manage exposed ports: - -```php -add(42); - -$container->setExposedPorts($ports); -``` - -## Rename: Changing the human readable name of a container - -Rename the container called vanilla1 to vanilla2: -```php -find('vanilla1'); -$manager->rename($container,'vanilla2'); -``` - - -## Exec: Run a process within an existing running container. - -Running a process inside a running container is done in two steps: create an exec instance (identified by a hash value) and starting that instance (and then reading the returned data). -Example: connect to the container called 'vanilla2', create an exec for 'ls /var/www/html' (within a bash shell) and run it: - -```php -find($containerName); -$execid = $manager->exec($container, ["/bin/bash", "-c", "ls /var/www/html"]); -$response = $manager->execstart($execid); - -print_r("Result= <" . $response->getBody()->__toString() . ">\n"); -``` - -Note that after an exec has been created it can be run several times, e.g. an exec for '/bin/date' would return a different value each time execstart() is called. - -You can also stream the result by using a callback during the execstart call : - -```php -find($containerName); -$execid = $manager->exec($container, ["/bin/bash", "-c", "ls /var/www/html"]); - -$response = $manager->execstart($execid, function ($log, $type) use($logger) { - // Log output in real time - $logger->info($log, array('type' => $type)); -}); - -//Response stream is never read you need to simulate a wait in order to get output -$response->getBody()->getContents(); -``` diff --git a/docs/cookbook/build-image.md b/docs/cookbook/build-image.md new file mode 100644 index 00000000..71a5d4bc --- /dev/null +++ b/docs/cookbook/build-image.md @@ -0,0 +1,81 @@ +# Building an Image + +In order to build an image you need to provide the `$inputStream` variable which correspond to the tar binary of a +folder containing a `Dockerfile` (or another name by using the `dockerfile` parameters) and other files used during the +build. + +Since `Docker` build directory can be heavy, Docker PHP override this call and allows passing a `resource` or a +`Psr\Http\Message\StreamInterface` instance (string is also possible but not recommended). +This avoid using too much memory in PHP. + +## Build return + +This function can return 3 different objects depending on the value of the `$fetch` parameter: + +### Docker::FETCH_OBJECT + +This is default mode, where this function will block until the build is finished and return an array of `BuildInfo` +object. + +This object contains the log of the build: + +```php +imageBuild($inputStream); +$buildStream->onFrame(function (BuildInfo $buildInfo) { + echo $buildInfo->getStream(); +}); + +$buildStream->wait(); +``` + +### Docker::FETCH_RESPONSE + +The build function will return the raw [PSR7](http://www.php-fig.org/psr/psr-7/) Response. It's up to you to handle +decoding and receiving correct output in this case. + +## Context + +Docker PHP provides a `ContextInterface` and a default `Context` object for creating the `$inputStream` of the build +method. + +```php +toStream(); +$docker = Docker::create(); + +$docker->imageBuild($inputStream); +``` + +You can safely use this context object to build image with a huge directory to size without consuming any memory or disk +on the PHP side as it will directly pipe the output of a `tar` process into the Docker Remote API. + +### Context Builder + +Additionally you can use the `ContextBuilder` to have a dynamic generation of your `Dockerfile`: + +```php +from('ubuntu:latest'); +$contextBuilder->run('apt-get update && apt-get install -y php5'); + +$docker = Docker::create(); +$docker->imageBuild($contextBuilder->getContext()->toStream()); +``` + diff --git a/docs/cookbook/container-run.md b/docs/cookbook/container-run.md new file mode 100644 index 00000000..2ee824bf --- /dev/null +++ b/docs/cookbook/container-run.md @@ -0,0 +1,251 @@ +# Running a container + +Running a container in Docker PHP like it would be done with the docker client `docker run image command` is not a +single call to api, even with the docker run command, it involves multiple calls to the API. + +## Creating the container + +First step is to create a container and its associated configuration, by creating a `ContainerConfig` instance and +passing to the `create` api endpoint. + +```php +setImage('busybox:latest'); +$containerConfig->setCmd(['echo', 'I am running a command']); + +$containerCreateResult = $docker->containerCreate($containerConfig); +``` + +This will return a `ContainersCreatePostResponse201` object with the id of the created container. If you don't want to use +container id you can also specify a unique name for this container: + +```php +$containerCreateResult = $docker->containerCreate($containerConfig, ['name' => 'my-container-unique-name']); +``` + +Be aware that the container is immutable if you need to change a configuration for a container, you will need to remove +the existing one and create it again with the new configuration. + +## Starting the container + +Once a container has been created, you can start it, this will in fact, only launch the process inside the isolated +container. This is done with the `containerStart` method, you can use the id of the container or the +name: + +```php +$docker->containerStart($containerCreateResult->getId()); +// Or +$docker->containerStart('my-container-unique-name'); +``` + +The start method will always return the raw [PSR7](http://www.php-fig.org/psr/psr-7/) Response, but you don't need +to check it as on failure it will throw an exception. + +## Waiting for the container to end + +Once your container is started you can wait for it end by using the `containerWait` method, be aware that your PHP script will +block until the container has stopped or that the default timeout set on the client has been reached (default to 60 +seconds) + +```php +$docker->containerWait('my-container-unique-name'); +``` + +## Stopping the container + +Once your container is started you can stop it by using the `containerStop` method. You can use the id of the container or the name: + +```php +$docker->containerStop($containerCreateResult->getId()); +// Or +$docker->containerStop('my-container-unique-name'); +``` + + +## Reading logs in real time + +Sometimes you will need to read logs in real time for a container. You can use the `containerAttach` method for that. +Be aware that you will only receive them if you configure the container with +[json log driver](https://docs.docker.com/engine/reference/logging/overview/), which is the default configuration. + +```php +$attachStream = $docker->containerAttach('my-container-unique-name'); +``` + +The `$attachStream` returned will be an instance of a `DockerRawStream`. You can use this object afterwards to add +callbacks on the different streams: + + * `addStdin` to add a callback on the stdin stream + * `addStdout` for the stdout stream + * `addStderr` for the stderr stream + +The callback for each of this method takes a string for the first argument which correspond to the log line. + +Use then the wait method to activate real time logging, this method will only stop when then stream is closed which +correspond to when the container has stopped. + +```php +$attachStream->onStdout(function ($stdout) { + echo $stdout; +}); +$attachStream->onStderr(function ($stderr) { + echo $stderr; +}); + +$attachStream->wait(); +``` + +If you follow all the example, you will not see the log and this normal. In fact the container and the call to the attach +method need extra configuration: + +```php +$containerConfig = new ContainersCreatePostBody(); +$containerConfig->setImage('busybox:latest'); +$containerConfig->setCmd(['echo', 'I am running a command']); +// You need to attach stream of the container to docker +$containerConfig->setAttachStdin(true); +$containerConfig->setAttachStdout(true); +$containerConfig->setAttachStderr(true); + +$docker->containerCreate($containerConfig, ['name' => 'my-container-unique-name']); + +// You also need to set stream to true to get the logs, and tell which stream you want to attach +$attachStream = $docker->containerAttach('my-container-unique-name', [ + 'stream' => true, + 'stdin' => true, + 'stdout' => true, + 'stderr' => true +]); +$docker->containerStart('my-container-unique-name'); + +$attachStream->onStdout(function ($stdout) { + echo $stdout; +}); +$attachStream->onStderr(function ($stderr) { + echo $stderr; +}); + +$attachStream->wait(); +``` + +If you read the following example, you will notice that we call the `containerAttach` method before starting the container with +`containerStart`. This is normal, as otherwise during the time the container is started and the call to the `containerAttach` endpoint +some logs may have been processed and you will loose this information. That's why it is strongly recommended to attach +the container before starting it. + +## Interacting with a container + +WIth the last example we can now read the log a container in real time. However you may need to send input to this +container. This can be done by attaching a websocket with the `containerAttachWebsocket` method:` + +```php +$webSocketStream = $docker->containerAttachWebsocket('my-container-unique-name', [ + 'stream' => true, + 'stdout' => true, + 'stderr' => true, + 'stdin' => true, +]); +``` + +The returned stream will be an instance of `AttachWebsocketStream` and it can be used to both reading and writing to +the container: + +```php +$line = $webSocketStream->read(); +$websocketStream->write('i send input to the container'); +``` + +If the `$line` is `null` then the stream is no longer available (container is certainly stopped), if it's equal to +`false` then the stream is still available but no output was received, otherwise it will return output from the container. + +To actually write on the stream (having the stdin stream open) you will, again, need extra configuration when creating +the container: + +```php +// Open the stdin stream from docker engine to the container +$containerConfig->setOpenStdin(true); +// Needed if you want to use process that rely on a tty, be aware as there is, in fact, no tty this may cause bug to +// the underlying process in your container +$containerConfig->setTty(true); +``` + +Be aware that there is no distinction between stdout and stderr in this mode. + +## Port Mapping + +This example shows how you can map port 8080 on your host machine to port 80 on the container. + +```php +setTty(true); +$containerConfig->setExposedPorts(['80/tcp' => new \stdClass]); + +$portBinding = new PortBinding(); +$portBinding->setHostPort('8080'); +$portBinding->setHostIp('0.0.0.0'); + +$portMap = new \ArrayObject(); +$portMap['80/tcp'] = [$portBinding]; + +$hostConfig = new HostConfig(); +$hostConfig->setPortBindings($portMap); + +$containerConfig->setHostConfig($hostConfig); +``` + +## Executing a command on a running container + +This example shows how you can execute an command on any running container. It is same as `docker exec CONTAINER_ID some_command `. + +```php +setTty(true); +$execConfig->setAttachStdout(true); +$execConfig->setAttachStderr(true); +$execConfig->setCmd(['mkdir', '/tmp/testDir']); + +$execid = $docker->containerExec('android',$execConfig)->getId(); +$execStartConfig = new ExecIdStartPostBody(); +$execStartConfig->setDetach(false); + +// Execute the command +$stream = $docker->execStart($execid,$execStartConfig); + +// To see the output stream of the 'exec' command +$stdoutText = ""; +$stderrText = ""; + +$stream->onStdout(function ($stdout) use (&$stdoutText) { + $stdoutText .= $stdout; +}); + +$stream->onStderr(function ($stderr) use (&$stderrText) { + $stderrText .= $stderr; +}); + +$stream->wait(); +var_dump([ "stdout" => $stdoutText, "stderr" => $stderrText ]) ; +``` diff --git a/docs/image/build.md b/docs/image/build.md deleted file mode 100644 index e69de29b..00000000 diff --git a/docs/image/find.md b/docs/image/find.md deleted file mode 100644 index 8de7c17a..00000000 --- a/docs/image/find.md +++ /dev/null @@ -1 +0,0 @@ -# Create an image diff --git a/docs/image/pull.md b/docs/image/pull.md deleted file mode 100644 index 7a965ce0..00000000 --- a/docs/image/pull.md +++ /dev/null @@ -1,13 +0,0 @@ -# Pull an image - -Using the [Docker client](../basic.md) you run - -```php - -$imageManager = $docker->getImageManager(); - -$ubuntuLatestImage = $imageManager->pull('ubuntu'); - -$php55Image = $imageManager->pull('php', '5.5'); - -``` \ No newline at end of file diff --git a/docs/image/remove.md b/docs/image/remove.md deleted file mode 100644 index 6666bed7..00000000 --- a/docs/image/remove.md +++ /dev/null @@ -1,32 +0,0 @@ -# Remove an image - -Using the [Docker client](../basic.md) you run - -```php -$imageManager = $docker->getImageManager(); - -$image1 = $manager->find('ubuntu', 'vivid'); - -$manager->remove($image1); - -// OR - -$image2 = new Docker\Image(); -$image2->setId('69c02692b0c1'); - -$manager->remove($image2); -``` - -The `remove` method has a second argument `$force` and a third one `$noprune`, both default to `false`. - -# Remove multiple images - -You can remove multiple images at once by passing an array of `Docker\Image` instances or strings (image id or image repository name and repository tag) to the `removeImages()` method. - -```php -$manager->removeImages([$image, 'ubuntu:vivid', '69c02692b0c1']); -``` - -The method has the same second (`$force`) and third (`$noprune`) argument as the `remove` method. - -Keep in mind that all containers which are based on these images have to be removed before the image itself can be removed. \ No newline at end of file diff --git a/docs/index.md b/docs/index.md index 2df80687..d00780dc 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,14 +1,22 @@ # Docker-PHP -This is the documentation for [docker-php](https://github.com/stage1/docker-php) library. - -* [Connecting to Docker](#connecting-to-docker) -* [Running a container](#running-a-container) -* [Streaming a running container's output](#streaming-a-running-containers-output) -* [Running a container as a daemon](#running-a-container-as-a-daemon) -* [Creating, starting, attaching and waiting for containers](#creating-starting-attaching-and-waiting-for-containers) -* [Mapping a container's ports](#mapping-a-containers-ports) -* [Finding and inspecting Containers](#finding-and-inspecting-containers) -* [Stopping and removing containers](#stopping-and-removing-containers) -* [The Docker\Container class](#the-dockercontainer-class) -* [Configuring exposed ports](#configuring-exposed-ports) +This is the documentation for [docker-php](https://github.com/docker-php/docker-php) library. + +This library aim to reach 100% API support of the Docker Engine. + +## Basics + +First you need to learn the basics of this library : + +* [Installation](installation.md) +* [Connecting to Docker](connection.md) +* [Basic usage](basic.md) +* [Asynchronous Client](async.md) + +## Cookbook + +The cookbook is a collection of recipes that explain how to solve common +problems and advanced usage when using Docker-PHP + +* [Running a container](cookbook/container-run.md) +* [Build an image](cookbook/build-image.md) diff --git a/docs/installation.md b/docs/installation.md index c43e9042..2cc4fcec 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -1,18 +1,21 @@ # Installation -The recommended way to install Docker PHP is of course to use [Composer](http://getcomposer.org/): +The recommended way to install Docker PHP is to use [Composer](http://getcomposer.org/): -Run `composer require "stage1/docker-php":"@dev"` to add the dependency +Run `composer require docker-php/docker-php` to add the dependency -or modify your composer.json manually: +By default it will use the last API version. However you can specify the API version of docker by setting a specific +version for the `docker-php/docker-php-api`. -```json -{ - "require": { - "stage1/docker-php": "@dev" - } -} +To use the 1.29 version you can do the following: + +``` +composer require docker-php/docker-php-api:4.1.29.* ``` +Do not use `^4.1.29.0`; otherwise, you will also depend on the latest version. The first digit of this version number matches the +major version of [Jane PHP](https://github.com/janephp/janephp), which is the lib generating the API Client code. -**Note**: there is no stable version of Docker PHP yet. +Note that some endpoints of the Docker API may have BC breaks during minor version updates. This library may +try to hide those BC breaks but it will always be a best effort. Feel free to raise an issue or pull request on github when +you encounter one. diff --git a/mkdocs.yml b/mkdocs.yml index f60bd771..1f127ad3 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -1,13 +1,15 @@ site_name: Docker PHP site_url: http://docker-php.readthedocs.org/ site_description: A Docker client in PHP -repo_url: https://github.com/stage1/docker-php +repo_url: https://github.com/docker-php/docker-php pages: - - [index.md, Home] - - [installation.md, 'Docker PHP', Installation] - - [basic.md, 'Docker PHP', Basic Usage] - - [image/build.md, Images, Build an image] - - [image/pull.md, Images, Pulling an image] - - [image/find.md, Images, Find an image] - - [container.md, Containers] + - Home: index.md + - Docker PHP: + - Installation: installation.md + - Connecting to Docker: connection.md + - Basic Usage: basic.md + - Asynchronous Client: async.md + - Cookbook: + - Running a container: cookbook/container-run.md + - Build an image: cookbook/build-image.md diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 9b0d412d..c4052566 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -15,8 +15,13 @@ - src/Docker/Tests/ + tests/ + + + src + + diff --git a/src/Client/AmpArtaxStreamEndpoint.php b/src/Client/AmpArtaxStreamEndpoint.php new file mode 100644 index 00000000..ec7f3b9a --- /dev/null +++ b/src/Client/AmpArtaxStreamEndpoint.php @@ -0,0 +1,26 @@ +transformResponseBody($chunk, $response->getStatus(), $serializer); + }; + } + + return new ArtaxCallbackStream($response->getBody(), $cancellationTokenSource, $responseTransformer); + }); + } +} diff --git a/src/Client/ProvideAmpArtaxClientOptions.php b/src/Client/ProvideAmpArtaxClientOptions.php new file mode 100644 index 00000000..cd1aa87d --- /dev/null +++ b/src/Client/ProvideAmpArtaxClientOptions.php @@ -0,0 +1,15 @@ +directory = $directory; $this->format = $format; + $this->fs = $fs ?? new Filesystem(); } /** - * Get directory of Context + * Get directory of Context. * * @return string */ @@ -55,31 +70,31 @@ public function getDirectory() } /** - * Set directory of Context + * Set directory of Context. * * @param string $directory Targeted directory */ - public function setDirectory($directory) + public function setDirectory($directory): void { $this->directory = $directory; } /** - * Return content of Dockerfile of this context + * Return content of Dockerfile of this context. * * @return string Content of dockerfile */ public function getDockerfileContent() { - return file_get_contents($this->directory.DIRECTORY_SEPARATOR.'Dockerfile'); + return \file_get_contents($this->directory.DIRECTORY_SEPARATOR.'Dockerfile'); } /** - * @return boolean + * @return bool */ public function isStreamed() { - return $this->format === self::FORMAT_STREAM; + return self::FORMAT_STREAM === $this->format; } /** @@ -91,7 +106,7 @@ public function read() } /** - * Return the context as a tar archive + * Return the context as a tar archive. * * @throws \Symfony\Component\Process\Exception\ProcessFailedException * @@ -110,15 +125,15 @@ public function toTar() } /** - * Return a stream for this context + * Return a stream for this context. * - * @return resouce Stream resource in memory + * @return resource Stream resource in memory */ public function toStream() { - if (!is_resource($this->process)) { - $this->process = proc_open("/usr/bin/env tar c .", [["pipe", "r"], ["pipe", "w"], ["pipe", "w"]], $pipes, $this->directory); - $this->stream = $pipes[1]; + if (!\is_resource($this->process)) { + $this->process = \proc_open('/usr/bin/env tar c .', [['pipe', 'r'], ['pipe', 'w'], ['pipe', 'w']], $pipes, $this->directory); + $this->stream = $pipes[1]; } return $this->stream; @@ -126,12 +141,24 @@ public function toStream() public function __destruct() { - if (is_resource($this->process)) { - proc_close($this->process); + if (\is_resource($this->stream)) { + \fclose($this->stream); + } + + if (\is_resource($this->process)) { + \proc_close($this->process); } - if (is_resource($this->stream)) { - fclose($this->stream); + if ($this->cleanup) { + $this->fs->remove($this->directory); } } + + /** + * @param bool $value whether to remove the context directory + */ + public function setCleanup(bool $value): void + { + $this->cleanup = $value; + } } diff --git a/src/Context/ContextBuilder.php b/src/Context/ContextBuilder.php new file mode 100644 index 00000000..74a4308b --- /dev/null +++ b/src/Context/ContextBuilder.php @@ -0,0 +1,396 @@ +fs = $fs ?: new Filesystem(); + $this->format = Context::FORMAT_STREAM; + } + + /** + * Sets the format of the Context output. + * + * @param string $format + * + * @return \Docker\Context\ContextBuilder + */ + public function setFormat($format) + { + $this->format = $format; + + return $this; + } + + /** + * Add a FROM instruction of Dockerfile. + * + * @param string $from From which image we start + * + * @return \Docker\Context\ContextBuilder + */ + public function from($from) + { + $this->commands[] = ['type' => 'FROM', 'image' => $from]; + + return $this; + } + + /** + * Set the CMD instruction in the Dockerfile. + * + * @param string $command Command to execute + * + * @return \Docker\Context\ContextBuilder + */ + public function command($command) + { + $this->command = $command; + + return $this; + } + + /** + * Set the ENTRYPOINT instruction in the Dockerfile. + * + * @param string $entrypoint The entrypoint + * + * @return \Docker\Context\ContextBuilder + */ + public function entrypoint($entrypoint) + { + $this->entrypoint = $entrypoint; + + return $this; + } + + /** + * Add an ADD instruction to Dockerfile. + * + * @param string $path Path wanted on the image + * @param string $content Content of file + * + * @return \Docker\Context\ContextBuilder + */ + public function add($path, $content) + { + $this->commands[] = ['type' => 'ADD', 'path' => $path, 'content' => $content]; + + return $this; + } + + /** + * Add an ADD instruction to Dockerfile. + * + * @param string $path Path wanted on the image + * @param resource $stream stream that contains file content + * + * @return \Docker\Context\ContextBuilder + */ + public function addStream($path, $stream) + { + $this->commands[] = ['type' => 'ADDSTREAM', 'path' => $path, 'stream' => $stream]; + + return $this; + } + + /** + * Add an ADD instruction to Dockerfile. + * + * @param string $path Path wanted on the image + * @param string $file Source file (or directory) name + * + * @return \Docker\Context\ContextBuilder + */ + public function addFile($path, $file) + { + $this->commands[] = ['type' => 'ADDFILE', 'path' => $path, 'file' => $file]; + + return $this; + } + + /** + * Add a RUN instruction to Dockerfile. + * + * @param string $command Command to run + * + * @return \Docker\Context\ContextBuilder + */ + public function run($command) + { + $this->commands[] = ['type' => 'RUN', 'command' => $command]; + + return $this; + } + + /** + * Add a ENV instruction to Dockerfile. + * + * @param string $name Name of the environment variable + * @param string $value Value of the environment variable + * + * @return \Docker\Context\ContextBuilder + */ + public function env($name, $value) + { + $this->commands[] = ['type' => 'ENV', 'name' => $name, 'value' => $value]; + + return $this; + } + + /** + * Add a COPY instruction to Dockerfile. + * + * @param string $from Path of folder or file to copy + * @param string $to Path of destination + * + * @return \Docker\Context\ContextBuilder + */ + public function copy($from, $to) + { + $this->commands[] = ['type' => 'COPY', 'from' => $from, 'to' => $to]; + + return $this; + } + + /** + * Add a WORKDIR instruction to Dockerfile. + * + * @param string $workdir Working directory + * + * @return \Docker\Context\ContextBuilder + */ + public function workdir($workdir) + { + $this->commands[] = ['type' => 'WORKDIR', 'workdir' => $workdir]; + + return $this; + } + + /** + * Add a EXPOSE instruction to Dockerfile. + * + * @param int $port Port to expose + * + * @return \Docker\Context\ContextBuilder + */ + public function expose($port) + { + $this->commands[] = ['type' => 'EXPOSE', 'port' => $port]; + + return $this; + } + + /** + * Adds an USER instruction to the Dockerfile. + * + * @param string $user User to switch to + * + * @return \Docker\Context\ContextBuilder + */ + public function user($user) + { + $this->commands[] = ['type' => 'USER', 'user' => $user]; + + return $this; + } + + /** + * Adds a VOLUME instruction to the Dockerfile. + * + * @param string $volume Volume path to add + * + * @return \Docker\Context\ContextBuilder + */ + public function volume($volume) + { + $this->commands[] = ['type' => 'VOLUME', 'volume' => $volume]; + + return $this; + } + + /** + * Create context given the state of builder. + * + * @return \Docker\Context\Context + */ + public function getContext() + { + $directory = \sys_get_temp_dir().'/ctb-'.\microtime(); + $this->fs->mkdir($directory); + $this->write($directory); + + $result = new Context($directory, $this->format, $this->fs); + $result->setCleanup(true); + + return $result; + } + + /** + * Write docker file and associated files in a directory. + * + * @param string $directory Target directory + * + * @void + */ + private function write($directory): void + { + $dockerfile = []; + // Insert a FROM instruction if the file does not start with one. + if (empty($this->commands) || $this->commands[0]['type'] !== 'FROM') { + $dockerfile[] = 'FROM base'; + } + foreach ($this->commands as $command) { + switch ($command['type']) { + case 'FROM': + $dockerfile[] = 'FROM '.$command['image']; + break; + case 'RUN': + $dockerfile[] = 'RUN '.$command['command']; + break; + case 'ADD': + $dockerfile[] = 'ADD '.$this->getFile($directory, $command['content']).' '.$command['path']; + break; + case 'ADDFILE': + $dockerfile[] = 'ADD '.$this->getFileFromDisk($directory, $command['file']).' '.$command['path']; + break; + case 'ADDSTREAM': + $dockerfile[] = 'ADD '.$this->getFileFromStream($directory, $command['stream']).' '.$command['path']; + break; + case 'COPY': + $dockerfile[] = 'COPY '.$command['from'].' '.$command['to']; + break; + case 'ENV': + $dockerfile[] = 'ENV '.$command['name'].' '.$command['value']; + break; + case 'WORKDIR': + $dockerfile[] = 'WORKDIR '.$command['workdir']; + break; + case 'EXPOSE': + $dockerfile[] = 'EXPOSE '.$command['port']; + break; + case 'VOLUME': + $dockerfile[] = 'VOLUME '.$command['volume']; + break; + case 'USER': + $dockerfile[] = 'USER '.$command['user']; + break; + } + } + + if (!empty($this->entrypoint)) { + $dockerfile[] = 'ENTRYPOINT '.$this->entrypoint; + } + + if (!empty($this->command)) { + $dockerfile[] = 'CMD '.$this->command; + } + + $this->fs->dumpFile($directory.DIRECTORY_SEPARATOR.'Dockerfile', \implode(PHP_EOL, $dockerfile)); + } + + /** + * Generate a file in a directory. + * + * @param string $directory Targeted directory + * @param string $content Content of file + * + * @return string Name of file generated + */ + private function getFile($directory, $content) + { + $hash = \md5($content); + + if (!\array_key_exists($hash, $this->files)) { + $file = \tempnam($directory, ''); + $this->fs->dumpFile($file, $content); + $this->files[$hash] = \basename($file); + } + + return $this->files[$hash]; + } + + /** + * Generated a file in a directory from a stream. + * + * @param string $directory Targeted directory + * @param resource $stream Stream containing file contents + * + * @return string Name of file generated + */ + private function getFileFromStream($directory, $stream) + { + $file = \tempnam($directory, ''); + $target = \fopen($file, 'w'); + if (0 === \stream_copy_to_stream($stream, $target)) { + throw new \RuntimeException('Failed to write stream to file'); + } + \fclose($target); + + return \basename($file); + } + + /** + * Generated a file in a directory from an existing file. + * + * @param string $directory Targeted directory + * @param string $source Path to the source file + * + * @return string Name of file generated + */ + private function getFileFromDisk($directory, $source) + { + $hash = 'DISK-'.\md5(\realpath($source)); + if (!\array_key_exists($hash, $this->files)) { + // Check if source is a directory or a file. + if (\is_dir($source)) { + $this->fs->mirror($source, $directory.'/'.$hash, null, ['copy_on_windows' => true]); + } else { + $this->fs->copy($source, $directory.'/'.$hash); + } + + $this->files[$hash] = $hash; + } + + return $this->files[$hash]; + } +} diff --git a/src/Docker/Context/ContextInterface.php b/src/Context/ContextInterface.php similarity index 82% rename from src/Docker/Context/ContextInterface.php rename to src/Context/ContextInterface.php index bb9b6ad6..0e1fc283 100644 --- a/src/Docker/Context/ContextInterface.php +++ b/src/Context/ContextInterface.php @@ -1,16 +1,18 @@ executePsr7Endpoint(new ContainerAttach($id, $queryParameters), $fetch); + } + + /** + * {@inheritdoc} + */ + public function containerAttachWebsocket(string $id, array $queryParameters = [], string $fetch = self::FETCH_OBJECT) + { + return $this->executePsr7Endpoint(new ContainerAttachWebsocket($id, $queryParameters), $fetch); + } + + /** + * {@inheritdoc} + */ + public function containerLogs(string $id, array $queryParameters = [], string $fetch = self::FETCH_OBJECT) + { + return $this->executePsr7Endpoint(new ContainerLogs($id, $queryParameters), $fetch); + } + + /** + * {@inheritdoc} + */ + public function execStart(string $id, \Docker\API\Model\ExecIdStartPostBody $execStartConfig, string $fetch = self::FETCH_OBJECT) + { + return $this->executePsr7Endpoint(new ExecStart($id, $execStartConfig), $fetch); + } + + /** + * {@inheritdoc} + */ + public function imageBuild($inputStream, array $queryParameters = [], array $headerParameters = [], string $fetch = self::FETCH_OBJECT) + { + return $this->executePsr7Endpoint(new ImageBuild($inputStream, $queryParameters, $headerParameters), $fetch); + } + + /** + * {@inheritdoc} + */ + public function imageCreate(string $inputImage, array $queryParameters = [], array $headerParameters = [], string $fetch = self::FETCH_OBJECT) + { + return $this->executePsr7Endpoint(new ImageCreate($inputImage, $queryParameters, $headerParameters), $fetch); + } + + /** + * {@inheritdoc} + */ + public function imagePush(string $name, array $queryParameters = [], array $headerParameters = [], string $fetch = self::FETCH_OBJECT) + { + if (isset($headerParameters['X-Registry-Auth']) && $headerParameters['X-Registry-Auth'] instanceof AuthConfig) { + $headerParameters['X-Registry-Auth'] = \base64_encode($this->serializer->serialize($headerParameters['X-Registry-Auth'], 'json')); + } + + return $this->executePsr7Endpoint(new ImagePush($name, $queryParameters, $headerParameters), $fetch); + } + + /** + * {@inheritdoc} + */ + public function systemEvents(array $queryParameters = [], string $fetch = self::FETCH_OBJECT) + { + return $this->executePsr7Endpoint(new SystemEvents($queryParameters), $fetch); + } + + public static function create($httpClient = null) + { + if (null === $httpClient) { + $httpClient = DockerClientFactory::createFromEnv(); + } + + return parent::create($httpClient); + } +} diff --git a/src/Docker/Container.php b/src/Docker/Container.php deleted file mode 100644 index 174a43d5..00000000 --- a/src/Docker/Container.php +++ /dev/null @@ -1,382 +0,0 @@ -config = $config; - } - - /** - * @return boolean - */ - public function exists() - { - return strlen($this->id) > 0; - } - - /** - * @return array - */ - public function getRuntimeInformations() - { - return $this->runtimeInformations; - } - - /** - * @param array $runtimeInformations - * - * @return Container - */ - public function setRuntimeInformations($runtimeInformations) - { - $this->runtimeInformations = $runtimeInformations; - - return $this; - } - - /** - * @return array - */ - public function getConfig() - { - return $this->config; - } - - /** - * @param integer $port - * @param string $protocol - * - * @return Port - */ - public function getMappedPort($port, $protocol = 'tcp') - { - // Problem with $protohuik as a variable name? Harass @futurecat. - $protohuik = $port.'/'.$protocol; - - if (!array_key_exists($protohuik, $this->runtimeInformations['NetworkSettings']['Ports'])) { - throw new PortNotFoundException($port, $protocol); - } - - $portInfo = $this->runtimeInformations['NetworkSettings']['Ports'][$protohuik]; - - return new Port(sprintf('%s:%s:%s/%s', $portInfo[0]['HostIp'], $portInfo[0]['HostPort'], $port, $protocol)); - } - - /** - * Accepts both (eg) 80 or 80/tcp as inputs. - * - * @param integer|string ...$ports - * - * @return array - */ - public function getMappedPorts() - { - $ports = func_get_args(); - $mappedPorts = []; - - foreach ($ports as $protohuik) { - // @todo better validation of $protohuik - // could use an instance of Port for example - $parts = explode('/', $protohuik); - - if (count($parts) === 1) { - $parts[] = 'tcp'; - } - - list($port, $protocol) = $parts; - - try { - $mappedPort = $this->getMappedPort($port, $protocol); - } catch (PortNotFoundException $e) { - continue; - } - - $mappedPorts[] = $mappedPort; - } - - return $mappedPorts; - } - - /** - * @param string $id - * - * @return Container - */ - public function setId($id) - { - $this->id = $id; - - return $this; - } - - /** - * @return string - */ - public function getId() - { - return $this->id; - } - - /** - * @return string - */ - public function getName() - { - if (array_key_exists('Name', $this->runtimeInformations)) { - return $this->runtimeInformations['Name']; - } - - if (isset($this->name)) { - return $this->name; - } - - return null; - } - - /** - * @param string $name - * - * @return Container - */ - public function setName($name) - { - if (!preg_match("/^\/?[a-zA-Z0-9_-]+$/", $name)) { - throw new \Exception("Name was not correctly formatted.", 1); - } - - $this->name = $name; - - return $this; - } - - /** - * @param integer $exitCode - * - * @return Container - */ - public function setExitCode($exitCode) - { - $this->exitCode = (integer) $exitCode; - - return $this; - } - - /** - * @return integer - */ - public function getExitCode() - { - if (null !== $this->exitCode) { - return $this->exitCode; - } - - if (is_array($this->runtimeInformations) && isset($this->runtimeInformations['State'])) { - return $this->runtimeInformations['State']['ExitCode']; - } - - throw new LogicException('Could not find an exit code'); - } - - /** - * @param Port $ports - * - * @return Container - */ - public function setExposedPorts($ports) - { - if ($ports instanceof PortSpecInterface) { - $this->config['ExposedPorts'] = $ports->toExposedPorts(); - } else { - $this->config['ExposedPorts'] = $ports; - } - - return $this; - } - - /** - * @param integer $memory - * - * @return Container - */ - public function setMemory($memory) - { - $this->config['Memory'] = (integer) $memory; - - return $this; - } - - /** - * @param string[] $env - * - * @return Container - */ - public function addEnv(array $env) - { - $this->config['Env'] = array_merge($this->getEnv(), $env); - - return $this; - } - - /** - * @param array $env - * - * @return Container - */ - public function setEnv(array $env) - { - $this->config['Env'] = $env; - - return $this; - } - - /** - * @return array - */ - public function getEnv() - { - if (isset($this->runtimeInformations['Config']['Env'])) { - return $this->runtimeInformations['Config']['Env']; - } - - if (isset($this->config['Env'])) { - return $this->config['Env']; - } - - return []; - } - - /** - * @return array - */ - public function getParsedEnv() - { - $env = []; - - foreach ($this->getEnv() as $raw) { - list($key, $value) = explode('=', $raw); - $env[$key] = $value; - } - - return $env; - } - - /** - * @param string|Image $image - * - * @return Container - */ - public function setImage($image) - { - if ($image instanceof Image) { - $this->image = $image; - } - - $this->config['Image'] = (string) $image; - - return $this; - } - - /** - * @return Image - */ - public function getImage() - { - if (!$this->image instanceof Image) { - $repository = $this->config['Image']; - $tag = 'latest'; - - if (preg_match('/:/', $this->config['Image'])) { - list($repository, $tag) = explode(':', $this->config['Image']); - } - - $this->image = new Image($repository, $tag); - } - - return $this->image; - } - - /** - * @param array $cmd - * - * @return Container - */ - public function setCmd(array $cmd) - { - $this->config['Cmd'] = $cmd; - - return $this; - } - - /** - * @param array $data - * - * @return Container - */ - public function setData(array $data) - { - $this->data = $data; - - return $this; - } - - /** - * @return array - */ - public function getData() - { - return $this->data; - } -} diff --git a/src/Docker/Context/ContextBuilder.php b/src/Docker/Context/ContextBuilder.php deleted file mode 100644 index 7d6b7874..00000000 --- a/src/Docker/Context/ContextBuilder.php +++ /dev/null @@ -1,184 +0,0 @@ -fs = $fs ?: new Filesystem(); - } - - /** - * Sets the format of the Context output - * - * @param string $format - * - * @return \Docker\Context\ContextBuilder - */ - public function setFormat($format) - { - $this->format = $format; - - return $this; - } - - /** - * Set the FROM instruction of Dockerfile - * - * @param string $from From which image we start - * - * @return \Docker\Context\ContextBuilder - */ - public function from($from) - { - $this->from = $from; - - return $this; - } - - /** - * Add a ADD instruction to Dockerfile - * - * @param string $path Path wanted on the image - * @param string $content Content of file - * - * @return \Docker\Context\ContextBuilder - */ - public function add($path, $content) - { - $this->commands[] = ['type' => 'ADD', 'path' => $path, 'content' => $content]; - - return $this; - } - - /** - * Add a RUN instruction to Dockerfile - * - * @param string $command Command to run - * - * @return \Docker\Context\ContextBuilder - */ - public function run($command) - { - $this->commands[] = ['type' => 'RUN', 'command' => $command]; - - return $this; - } - - /** - * Create context given the state of builder - * - * @return \Docker\Context\Context - */ - public function getContext() - { - if ($this->directory !== null) { - $this->cleanDirectory(); - } - - $this->directory = sys_get_temp_dir().'/'.md5($this->from.serialize($this->commands)); - $this->fs->mkdir($this->directory); - $this->write($this->directory); - - return new Context($this->directory, $this->fs, $this->format); - } - - /** - * @void - */ - public function __destruct() - { - $this->cleanDirectory(); - } - - /** - * Write docker file and associated files in a directory - * - * @param string $directory Target directory - * - * @void - */ - private function write($directory) - { - $dockerfile = []; - $dockerfile[] = 'FROM '.$this->from; - - foreach ($this->commands as $command) { - switch ($command['type']) { - case 'RUN': - $dockerfile[] = 'RUN '.$command['command']; - break; - case 'ADD': - $dockerfile[] = 'ADD '.$this->getFile($directory, $command['content']).' '.$command['path']; - break; - } - } - - $this->fs->dumpFile($directory.DIRECTORY_SEPARATOR.'Dockerfile', implode(PHP_EOL, $dockerfile)); - } - - /** - * Generated a file in a directory - * - * @param string $directory Targeted directory - * @param string $content Content of file - * - * @return string Name of file generated - */ - private function getFile($directory, $content) - { - $hash = md5($content); - - if (!array_key_exists($hash, $this->files)) { - $file = tempnam($directory, ''); - $this->fs->dumpFile($file, $content); - $this->files[$hash] = basename($file); - } - - return $this->files[$hash]; - } - - /** - * Clean directory generated - */ - private function cleanDirectory() - { - $this->fs->remove($this->directory); - } -} diff --git a/src/Docker/Docker.php b/src/Docker/Docker.php deleted file mode 100644 index 45c8fad2..00000000 --- a/src/Docker/Docker.php +++ /dev/null @@ -1,180 +0,0 @@ -httpClient = $httpClient ?: new DockerClient(); - } - - /** - * @return \GuzzleHttp\Client - */ - public function getHttpClient() - { - return $this->httpClient; - } - - /** - * @return \Docker\Manager\ContainerManager - */ - public function getContainerManager() - { - if (null === $this->containerManager) { - $this->containerManager = new ContainerManager($this->httpClient); - } - - return $this->containerManager; - } - - /** - * @return \Docker\Manager\ImageManager - */ - public function getImageManager() - { - if (null === $this->imageManager) { - $this->imageManager = new ImageManager($this->httpClient); - } - - return $this->imageManager; - } - - /** - * Show the docker components version information - * @return array json object with version values - */ - public function getVersion() - { - try { - $response = $this->httpClient->get(['/version', []]); - } catch (RequestException $e) { - throw $e; - } - - return $response->json(); - } - - /** - * Docker info: Display system-wide information - * api_v1.16 - * @return array json object with version values - */ - public function getInfo() - { - try { - $response = $this->httpClient->get(['/info', []]); - } catch (RequestException $e) { - throw $e; - } - - return $response->json(); - } - - - /** - * Build an image with docker - * - * @param \Docker\Context\ContextInterface $context Context to build - * @param string $name Name of the wanted image - * @param callable $callback A callback to be called for having log of build - * @param boolean $quiet Quiet build (doest not output commands during build) - * @param boolean $cache Use docker cache - * @param boolean $rm Remove intermediate container during build - * @param boolean $wait Whether to wait for build to finish - * - * @return \GuzzleHttp\Message\ResponseInterface - */ - public function build(ContextInterface $context, $name, callable $callback = null, $quiet = false, $cache = true, $rm = false, $wait = true) - { - if (null === $callback) { - $callback = function () {}; - } - - $content = is_resource($context->read()) ? new Stream($context->read()) : $context->read(); - - return $this->httpClient->post(['/build{?data*}', ['data' => [ - 'q' => (integer) $quiet, - 't' => $name, - 'nocache' => (integer) !$cache, - 'rm' => (integer) $rm, - ]]], [ - 'headers' => ['Content-Type' => 'application/tar'], - 'body' => $content, - 'stream' => true, - 'callback' => $callback, - 'wait' => $wait - ]); - } - - /** - * Commit a container into an image - * - * @param \Docker\Container $container - * @param array $config - * - * @throws Exception\UnexpectedStatusCodeException - * - * @return \Docker\Image - * - * @see http://docs.docker.com/reference/api/docker_remote_api_v1.7/#create-a-new-image-from-a-containers-changes - */ - public function commit(Container $container, $config = []) - { - if (isset($config['run'])) { - $config['run'] = json_encode($config['run']); - } - - $config['container'] = $container->getId(); - - $response = $this->httpClient->post(['/commit{?config*}', ['config' => $config]]); - - if ($response->getStatusCode() !== "201") { - throw new UnexpectedStatusCodeException($response->getStatusCode(), (string) $response->getBody()); - } - - $image = new Image(); - $image->setId($response->json()['Id']); - - if (array_key_exists('repo', $config)) { - $image->setRepository($config['repo']); - } - - if (array_key_exists('tag', $config)) { - $image->setTag($config['tag']); - } - - return $image; - } -} diff --git a/src/Docker/Exception.php b/src/Docker/Exception.php deleted file mode 100644 index 5e593522..00000000 --- a/src/Docker/Exception.php +++ /dev/null @@ -1,12 +0,0 @@ -getStatusCode(), trim((string) $response->getBody())); - } -} diff --git a/src/Docker/Http/Adapter/DockerAdapter.php b/src/Docker/Http/Adapter/DockerAdapter.php deleted file mode 100644 index 282a6082..00000000 --- a/src/Docker/Http/Adapter/DockerAdapter.php +++ /dev/null @@ -1,319 +0,0 @@ -entrypoint = $entrypoint; - $this->messageFactory = $messageFactory; - $this->context = $context; - $this->useTls = $useTls; - - stream_filter_register('chunk', '\Docker\Http\Stream\Filter\Chunk'); - stream_filter_register('event', '\Docker\Http\Stream\Filter\Event'); - } - - /** - * Transfers an HTTP request and populates a response - * - * @param TransactionInterface $transaction Transaction abject to populate - * - * @return ResponseInterface - */ - public function send(TransactionInterface $transaction) - { - // HTTP/1.1 streams using the PHP stream wrapper require a - // Connection: close header. Setting here so that it is added before - // emitting the request.before_send event. - $request = $transaction->getRequest(); - if ($request->getProtocolVersion() == '1.1' && - !$request->hasHeader('Connection') - ) { - $transaction->getRequest()->setHeader('Connection', 'close'); - } - - try { - RequestEvents::emitBefore($transaction); - if (!$transaction->getResponse()) { - $this->createResponse($transaction); - RequestEvents::emitComplete($transaction); - } - - return $transaction->getResponse(); - } catch (RequestException $e) { - if ($e->hasResponse() && $e->getResponse()->getBody()) { - throw new APIException($e->getResponse()->getBody()->__toString(), $e->getRequest(), $e->getResponse(), $e); - } - - throw $e; - } - } - - private function createResponse(TransactionInterface $transaction) - { - $errorNo = null; - $errorMsg = null; - - $request = $transaction->getRequest(); - $config = $request->getConfig(); - - if (isset($config['stream']) && $config['stream']) { - $request->setHeader('Transfer-Encoding', 'chunked'); - } elseif ($request->getBody() !== null) { - $request->setHeader('Content-Length', $request->getBody()->getSize()); - } - - $socket = @stream_socket_client($this->entrypoint, $errorNo, $errorMsg, $this->getDefaultTimeout($transaction), STREAM_CLIENT_CONNECT, $this->context); - - if (!$socket) { - throw new RequestException(sprintf('Cannot open socket connection: %s [code %d] [%s]', $errorMsg, $errorNo, $this->entrypoint), $request); - } - - // Check if tls is needed - if ($this->useTls) { - if (!@stream_socket_enable_crypto($socket, true, STREAM_CRYPTO_METHOD_TLS_CLIENT)) { - throw new RequestException(sprintf('Cannot enable tls: %s', error_get_last()['message']), $request); - } - } - - // Write headers - $isWrite = $this->fwrite($socket, $this->getRequestHeaderAsString($request)); - - // Write body if set - if ($request->getBody() !== null && $isWrite !== false) { - $stream = $request->getBody(); - $filter = null; - - if ($request->getHeader('Transfer-Encoding') == 'chunked') { - $filter = stream_filter_prepend($socket, 'chunk', STREAM_FILTER_WRITE); - } - - while (!$stream->eof() && $isWrite) { - $isWrite = $this->fwrite($socket, $stream->read(self::CHUNK_SIZE)); - } - - if ($request->getHeader('Transfer-Encoding') == 'chunked') { - stream_filter_remove($filter); - - if (false !== $isWrite) { - $isWrite = $this->fwrite($socket, "0\r\n\r\n"); - } - } - } - - stream_set_timeout($socket, $this->getDefaultTimeout($transaction)); - - // Response should be available, extract headers - do { - $response = $this->getResponseWithHeaders($socket); - } while ($response !== null && $response->getStatusCode() == 100); - - // Check timeout - $metadata = stream_get_meta_data($socket); - - if ($metadata['timed_out']) { - throw new RequestException('Timed out while reading socket', $request, $response); - } - - if (false === $isWrite) { - // When an error happen and no response it is most probably due to TLS configuration - if ($response === null) { - throw new RequestException('Error while sending request (Broken Pipe), check your TLS configuration and logs in docker daemon for more information ', $request); - } - - throw new RequestException('Error while sending request (Broken Pipe)', $request, $response); - } - - if (null == $response) { - throw new RequestException('No response could be parsed: check server log', $request); - } - - $this->setResponseStream($response, $socket, $request->getEmitter(), ($config->hasKey('attach_filter') && $config->get('attach_filter'))); - $transaction->setResponse($response); - - // If wait read all contents - if ($config->hasKey('wait') && $config->get('wait')) { - $response->getBody()->getContents(); - } - - return $response; - } - - private function getResponseWithHeaders($stream) - { - $headers = []; - - while (($line = fgets($stream)) !== false) { - if (rtrim($line) === '') { - break; - } - - $headers[] = trim($line); - } - - $parts = explode(' ', array_shift($headers), 3); - - if (count($parts) <= 1) { - return null; - } - - $options = ['protocol_version' => substr($parts[0], -3)]; - if (isset($parts[2])) { - $options['reason_phrase'] = $parts[2]; - } - - // Set the size on the stream if it was returned in the response - $responseHeaders = []; - foreach ($headers as $header) { - $headerParts = explode(':', $header, 2); - $responseHeaders[trim($headerParts[0])] = isset($headerParts[1]) - ? trim($headerParts[1]) - : ''; - } - - $response = new Response($parts[1], $responseHeaders, null, $options); - - return $response; - } - - private function setResponseStream(Response $response, $socket, EmitterInterface $emitter, $useFilter = false) - { - if ($response->getHeader('Transfer-Encoding') == "chunked") { - stream_filter_append($socket, 'dechunk'); - } - - // Attach filter - if ($useFilter) { - stream_filter_append($socket, 'event', STREAM_FILTER_READ, [ - 'emitter' => $emitter, - 'content_type' => $response->getHeader('Content-Type'), - ]); - } - - $stream = new Stream($socket); - $response->setBody($stream); - } - - private function getDefaultTimeout(TransactionInterface $transaction) - { - $timeout = $transaction->getRequest()->getConfig()->get('timeout'); - - if ($timeout !== null) { - return $timeout; - } - - $timeout = $transaction->getClient()->getDefaultOption('timeout'); - - if (null == $timeout) { - $timeout = ini_get('default_socket_timeout'); - } - - return $timeout; - } - - private function getRequestHeaderAsString(RequestInterface $request) - { - $message = vsprintf('%s %s HTTP/%s', [ - strtoupper($request->getMethod()), - $request->getUrl(), - $request->getProtocolVersion() - ])."\r\n"; - - foreach ($request->getHeaders() as $name => $values) { - $message .= $name.': '.implode(', ', $values)."\r\n"; - } - - $message .= "\r\n"; - - return $message; - } - - /** - * Replace fwrite behavior as api is broken in PHP - * - * @see https://secure.phabricator.com/rPHU69490c53c9c2ef2002bc2dd4cecfe9a4b080b497 - * - * @param resource $stream The stream resource - * @param string $bytes Bytes written in the stream - * - * @return bool|int false if pipe is broken, number of bytes written otherwise - */ - private function fwrite($stream, $bytes) - { - if (!strlen($bytes)) { - return 0; - } - - $result = @fwrite($stream, $bytes); - if ($result !== 0) { - // In cases where some bytes are witten (`$result > 0`) or - // an error occurs (`$result === false`), the behavior of fwrite() is - // correct. We can return the value as-is. - return $result; - } - - // If we make it here, we performed a 0-length write. Try to distinguish - // between EAGAIN and EPIPE. To do this, we're going to `stream_select()` - // the stream, write to it again if PHP claims that it's writable, and - // consider the pipe broken if the write fails. - - $read = []; - $write = [$stream]; - $except = []; - - @stream_select($read, $write, $except, 0); - - if (!$write) { - // The stream isn't writable, so we conclude that it probably really is - // blocked and the underlying error was EAGAIN. Return 0 to indicate that - // no data could be written yet. - return 0; - } - - // If we make it here, PHP **just** claimed that this stream is writable, so - // perform a write. If the write also fails, conclude that these failures are - // EPIPE or some other permanent failure. - $result = @fwrite($stream, $bytes); - if ($result !== 0) { - // The write worked or failed explicitly. This value is fine to return. - return $result; - } - - // We performed a 0-length write, were told that the stream was writable, and - // then immediately performed another 0-length write. Conclude that the pipe - // is broken and return `false`. - return false; - } -} diff --git a/src/Docker/Http/DockerClient.php b/src/Docker/Http/DockerClient.php deleted file mode 100644 index dabdddbe..00000000 --- a/src/Docker/Http/DockerClient.php +++ /dev/null @@ -1,98 +0,0 @@ - [ - 'cafile' => $cafile, - 'local_cert' => $fullcert, - 'peer_name' => $peername, - ], - ]); - } - - return new self($config, $entrypoint, $context, $useTls); - } -} diff --git a/src/Docker/Http/MessageFactory.php b/src/Docker/Http/MessageFactory.php deleted file mode 100644 index ce5aac21..00000000 --- a/src/Docker/Http/MessageFactory.php +++ /dev/null @@ -1,24 +0,0 @@ -getEmitter()->on('response.output', function (OutputEvent $event) use ($callback) { - $callback($event->getContent(), $event->getType()); - }); - - $request->getConfig()->set('attach_filter', true); - } - - protected function add_wait(RequestInterface $request, $wait) - { - $request->getConfig()->set('wait', $wait); - } -} diff --git a/src/Docker/Http/Stream/Filter/Chunk.php b/src/Docker/Http/Stream/Filter/Chunk.php deleted file mode 100644 index 52f91ea1..00000000 --- a/src/Docker/Http/Stream/Filter/Chunk.php +++ /dev/null @@ -1,22 +0,0 @@ -stream, dechex($bucket->datalen)."\r\n"); - stream_bucket_append($out, $lenbucket); - - $consumed += $bucket->datalen; - stream_bucket_append($out, $bucket); - - $lenbucket = stream_bucket_new($this->stream, "\r\n"); - stream_bucket_append($out, $lenbucket); - } - - return PSFS_PASS_ON; - } -} diff --git a/src/Docker/Http/Stream/Filter/Event.php b/src/Docker/Http/Stream/Filter/Event.php deleted file mode 100644 index ce361e64..00000000 --- a/src/Docker/Http/Stream/Filter/Event.php +++ /dev/null @@ -1,151 +0,0 @@ -emitter; - } - - /** - * Function call when stream is filtered - * - * @param resource $in Input stream - * @param resource $out Output stream - * @param integer $consumed Data consumed - * @param boolean $closing Whether the stream is closing - * - * @return int - */ - public function filter($in, $out, &$consumed, $closing) - { - $bucket = stream_bucket_make_writeable($in); - - if (null == $bucket) { - return PSFS_PASS_ON; - } - - $consumed = $bucket->datalen; - stream_bucket_append($out, $bucket); - - $data = $this->buffer . $bucket->data; - $type = null; - - if ($this->contentType == "application/vnd.docker.raw-stream") { - if (strlen($data) < 8) { - $this->buffer = $data; - - return PSFS_FEED_ME; - } - - $header = substr($data, 0, 8); - $decoded = unpack('C1stream_type/C3/N1size', $header); - - if (strlen($data) < (8 + $decoded['size'])) { - $this->buffer = $data; - - return PSFS_FEED_ME; - } - - $data = substr($data, 8, $decoded['size']); - $type = $decoded['stream_type']; - $this->buffer = substr($data, 8 + $decoded['size']); - - if (!empty($data)) { - $this->getEmitter()->emit('response.output', new OutputEvent($data, $type)); - } - - return PSFS_PASS_ON; - } - - if ($this->contentType == "application/json") { - foreach ($this->jsonSplitDecode($data) as $data) { - $this->getEmitter()->emit('response.output', new OutputEvent($data, $type)); - } - - return PSFS_PASS_ON; - } - - if (!empty($data)) { - $this->getEmitter()->emit('response.output', new OutputEvent($data, $type)); - } - - return PSFS_PASS_ON; - } - - /** - * Call when filter is created (attach to a socket) - * - * Here we set parameters to this instance - */ - public function onCreate() - { - $this->emitter = $this->params['emitter']; - $this->contentType = $this->params['content_type']; - } - - private function jsonSplitDecode($json) - { - $splited = []; - $size = strlen($json); - $inquote = false; - - for ($level = 0, $objects = 0, $i =0; $i < $size; $i++) { - if ((boolean)($json[$i] == '"' && ($i > 0 ? $json[$i-1] : '') != '\\')) { - $inquote = !$inquote; - } - - if (!$inquote && in_array($json[$i], [" ", "\r", "\n", "\t"])) { - continue; - } - - if (!$inquote && in_array($json[$i], ['{', '['])) { - $level++; - } - - if (!$inquote && in_array($json[$i], ['}', ']'])) { - $level--; - - if ($level == 0) { - $splited[$objects] .= $json[$i]; - $objects++; - continue; - } - } - - if (!isset($splited[$objects])) { - $splited[$objects] = ""; - } - - $splited[$objects] .= $json[$i]; - } - - foreach ($splited as $key => $jsonString) { - $splited[$key] = json_decode($jsonString, true); - } - - return $splited; - } -} diff --git a/src/Docker/Http/Stream/Filter/OutputEvent.php b/src/Docker/Http/Stream/Filter/OutputEvent.php deleted file mode 100644 index 8313ecc8..00000000 --- a/src/Docker/Http/Stream/Filter/OutputEvent.php +++ /dev/null @@ -1,56 +0,0 @@ -content = $content; - $this->type = $type; - } - - /** - * {@inheritdoc} - */ - public function isPropagationStopped() - { - return $this->stopped; - } - - /** - * {@inheritdoc} - */ - public function stopPropagation() - { - $this->stopped = true; - } - - /** - * @return null - */ - public function getType() - { - return $this->type; - } - - /** - * @return mixed - */ - public function getContent() - { - return $this->content; - } -} diff --git a/src/Docker/Http/Stream/InteractiveStream.php b/src/Docker/Http/Stream/InteractiveStream.php deleted file mode 100644 index 02457e7f..00000000 --- a/src/Docker/Http/Stream/InteractiveStream.php +++ /dev/null @@ -1,170 +0,0 @@ -detach(); - - $this->socket = $socket; - $this->stream = new Stream($socket); - } - - public function write($data) - { - $rand = rand(0, 28); - $frame = [ - 'fin' => 1, - 'rsv1' => 0, - 'rsv2' => 0, - 'rsv3' => 0, - 'opcode' => 1, // We always send text - 'mask' => 1, - 'len' => strlen($data), - 'mask_key' => substr(md5(uniqid()), $rand, 4), - 'data' => $data, - ]; - - if ($frame['mask'] == 1) { - for ($i = 0; $i < $frame['len']; $i++) { - $frame['data']{$i} - = chr(ord($frame['data']{$i}) ^ ord($frame['mask_key']{$i % 4})); - } - } - - if ($frame['len'] > pow(2, 16)) { - $len = 127; - } elseif ($frame['len'] > 125) { - $len = 126; - } else { - $len = $frame['len']; - } - - $firstByte = ($frame['fin'] << 7) | (($frame['rsv1'] << 7) >> 1) | (($frame['rsv2'] << 7) >> 2) | (($frame['rsv3'] << 7) >> 3) | (($frame['opcode'] << 4) >> 4); - $secondByte = ($frame['mask'] << 7) | (($len << 1) >> 1); - - $this->stream->write(chr($firstByte)); - $this->stream->write(chr($secondByte)); - - if ($len == 126) { - $this->stream->write(pack('n', $frame['len'])); - } elseif ($len == 127) { - $higher = $frame['len'] >> 32; - $lower = ($frame['len'] << 32) >> 32; - - $this->stream->write(pack('N', $higher)); - $this->stream->write(pack('N', $lower)); - } - - if ($frame['mask'] == 1) { - $this->stream->write($frame['mask_key']); - } - - $this->stream->write($frame['data']); - } - - /** - * Block until it receive a frame from websocket or return null if no more connexion - * - * @param bool $block - * @return array - */ - public function receive($block = true) - { - if ($this->stream->eof()) { - return null; - } - - $firstByte = $this->stream->read(1); - - if (!$block && empty($firstByte)) { - return null; - } - - if ($block && empty($firstByte)) { - $firstByte = $this->read(1); - } - - $frame = []; - $firstByte = ord($firstByte); - $secondByte = ord($this->read(1)); - - // First byte decoding - $frame['fin'] = ($firstByte & 128) >> 7; - $frame['rsv1'] = ($firstByte & 64) >> 6; - $frame['rsv2'] = ($firstByte & 32) >> 5; - $frame['rsv3'] = ($firstByte & 16) >> 4; - $frame['opcode'] = ($firstByte & 15); - - // Second byte decoding - $frame['mask'] = ($secondByte & 128) >> 7; - $frame['len'] = ($secondByte & 127); - - if ($frame['len'] == 126) { - $frame['len'] = unpack('n', $this->read(2))[1]; - } elseif ($frame['len'] == 127) { - list($higher, $lower) = array_values(unpack('N2', $this->read(8))); - $frame['len'] = ($higher << 32) | $lower; - } - - if ($frame['mask'] == 1) { - $frame['mask_key'] = $this->read(4); - } - - $frame['data'] = $this->read($frame['len']); - - if ($frame['mask'] == 1) { - for ($i = 0; $i < $frame['len']; $i++) { - $frame['data']{$i} - = chr(ord($frame['data']{$i}) ^ ord($frame['mask_key']{$i % 4})); - } - } - - return $frame; - } - - public function getStream() - { - return $this->stream; - } - - public function getSocket() - { - return $this->socket; - } - - /** - * Force to have something of the expected size (block) - * - * @param $length - * - * @return string - */ - private function read($length) - { - $read = ""; - - do { - $read .= $this->stream->read($length - strlen($read)); - } while (strlen($read) < $length); - - return $read; - } -} diff --git a/src/Docker/Image.php b/src/Docker/Image.php deleted file mode 100644 index 7369933b..00000000 --- a/src/Docker/Image.php +++ /dev/null @@ -1,110 +0,0 @@ -repository = $repository; - $this->tag = $tag; - } - - /** - * @return string - */ - public function __toString() - { - if (strlen($this->getRepository()) === 0) { - return $this->getId(); - } - - if (strlen($this->getTag()) === 0) { - return $this->getRepository(); - } - - return sprintf('%s:%s', $this->getRepository(), $this->getTag()); - } - - /** - * @return string - */ - public function getRepository() - { - return $this->repository; - } - - /** - * @param string $repository - * - * @return Image - */ - public function setRepository($repository) - { - $this->repository = $repository; - - return $this; - } - - /** - * @param string $id - * - * @return Image - */ - public function setId($id) - { - $this->id = $id; - - return $this; - } - - /** - * @return string - */ - public function getId() - { - return $this->id; - } - - /** - * @param string $tag - * - * @return Image - */ - public function setTag($tag = 'latest') - { - $this->tag = $tag; - - return $this; - } - - /** - * @return string - */ - public function getTag() - { - return $this->tag; - } -} diff --git a/src/Docker/Json.php b/src/Docker/Json.php deleted file mode 100644 index 9ddfa744..00000000 --- a/src/Docker/Json.php +++ /dev/null @@ -1,22 +0,0 @@ -client = $client; - } - - /** - * Get all containers from the docker daemon - * - * @param array $params an array of query parameters - * - * @throws \Docker\Exception\UnexpectedStatusCodeException - * - * @return Container[] - */ - public function findAll(array $params = []) - { - $response = $this->client->get('/containers/json', [ - 'query' => $params - ]); - - if ($response->getStatusCode() !== "200") { - throw UnexpectedStatusCodeException::fromResponse($response); - } - - $coll = []; - - $containers = $response->json(); - - if (!is_array($containers)) { - return []; - } - - foreach ($containers as $data) { - $container = new Container(); - $container->setId($data['Id']); - $container->setImage($data['Image']); - $container->setCmd((array) $data['Command']); - - $container->setData($data); - - $coll[] = $container; - } - - return $coll; - } - - /** - * Find a container by its id - * - * @param string $id - * - * @return \Docker\Container|null - */ - public function find($id) - { - $container = new Container(); - $container->setId($id); - - try { - $this->inspect($container); - } catch (ContainerNotFoundException $e) { - return null; - } - - return $container; - } - - /** - * Inspect a container - * - * @param \Docker\Container $container - * - * @throws \GuzzleHttp\Exception\RequestException - * @throws \Docker\Exception\ContainerNotFoundException - * - * @return array json data from docker inspect - */ - public function inspect(Container $container) - { - try { - $response = $this->client->get(['/containers/{id}/json', [ - 'id' => $container->getId() - ]]); - } catch (RequestException $e) { - if ($e->hasResponse() && $e->getResponse()->getStatusCode() == "404") { - throw new ContainerNotFoundException($container->getId(), $e); - } - - throw $e; - } - - $container->setRuntimeInformations($response->json()); - - return $response->json(); - } - - /** - * Create a container (do not start it) - * - * @param \Docker\Container $container - * - * @throws \Docker\Exception\UnexpectedStatusCodeException - * - * @return \Docker\Manager\ContainerManager - */ - public function create(Container $container) - { - $response = $this->client->post(['/containers/create{?data*}', [ - 'data' => [ - 'name' => $container->getName(), - ], - ]],[ - 'body' => Json::encode($container->getConfig()), - 'headers' => ['content-type' => 'application/json'], - ]); - - if ($response->getStatusCode() !== "201") { - throw UnexpectedStatusCodeException::fromResponse($response); - } - - $container->setId($response->json()['Id']); - - return $this; - } - - /** - * @param \Docker\Container $container - * @param array $hostConfig Config when starting the container (for port binding e.g.) - * - * @throws \Docker\Exception\UnexpectedStatusCodeException - * - * @return \Docker\Manager\ContainerManager - */ - public function start(Container $container, array $hostConfig = []) - { - $response = $this->client->post(['/containers/{id}/start', [ - 'id' => $container->getId() - ]],[ - 'body' => Json::encode($hostConfig), - 'headers' => ['content-type' => 'application/json'], - 'wait' => true, - ]); - - if ($response->getStatusCode() !== "204") { - throw UnexpectedStatusCodeException::fromResponse($response); - } - - $this->inspect($container); - - return $this; - } - - /** - * Run a container (create, attach, start and wait) - * - * @param \Docker\Container $container - * @param callable $attachCallback Callback to read the attach response - * If set to null no attach call will be made, otherwise callback must respect the format for the readAttach - * method in Docker\Http\Response class - * - * @param array $hostConfig Config when starting the container (for port binding e.g.) - * @param boolean $daemon Do not wait for run to finish - * @param integer $timeout Timeout pass to the attach call - * - * @return boolean|null Return true when the process want well, false if an error append during the run process, or null when daemon is set to true - */ - public function run(Container $container, callable $attachCallback = null, array $hostConfig = [], $daemon = false, $timeout = null) - { - $this->create($container); - - if (null !== $attachCallback) { - $attachResponse = $this->attach($container, $attachCallback, true, true, true, true, true, $timeout); - } - - $this->start($container, $hostConfig); - - if (!$daemon) { - if (isset($attachResponse)) { - $attachResponse->getBody()->getContents(); - } - - $this->wait($container); - - return ($container->getExitCode() == 0); - } - - return null; - } - - /** - * Execute a command in a running container - * i.e. create an executive, which will be run by execstart() - * - * @param \Docker\Container $container - * @param array $cmd command to run - * @param boolean $attachstdin - * @param boolean $attachstdout - * @param boolean $attachstderr - * @param boolean $tty - * - * @throws \Docker\Exception\UnexpectedStatusCodeException - * - * @return string ID of the executive - */ - public function exec(Container $container, array $cmd = [], $attachstdin = false, $attachstdout = true, $attachstderr = true, $tty = false) - { - $body = [ - 'AttachStdin' => $attachstdin, - 'AttachStdout' => $attachstdout, - 'AttachStderr' => $attachstderr, - 'Tty' => $tty, - 'Cmd' => $cmd - ]; - $response = $this->client->post(['/containers/{id}/exec', [ - 'id' => $container->getId() - ]], [ - 'body' => Json::encode($body), - 'headers' => ['content-type' => 'application/json'], - ]); - - if ($response->getStatusCode() !== "201") { - throw UnexpectedStatusCodeException::fromResponse($response); - } - - return $response->json()['Id']; - } - - /** - * Start an executive defined from exec() - * This can be resude several times, so if the command /bin/date in defined in exec() - * execstart() on that ID will return a different value each time. - * todo: how are instances created by exec() and used by execstart() removed/cleanedup? - * - * @param string $execid identifier from exec() - * @param callable $callback - * @param boolean $detach - * @param boolean $tty - * - * @throws \Docker\Exception\UnexpectedStatusCodeException - * - * @return \GuzzleHttp\Message\ResponseInterface - */ - public function execstart($execid, callable $callback = null, $detach = false, $tty = false) - { - $body = ['Detach' => $detach, 'Tty' => $tty ]; - $callback = $callback === null ? function() {} : $callback; - $response = $this->client->post(['/exec/{id}/start', [ - 'id' => $execid - ]], [ - 'body' => Json::encode($body), - 'headers' => ['content-type' => 'application/json'], - 'callback' => $callback, - ]); - - if ($response->getStatusCode() !== "200") { - throw UnexpectedStatusCodeException::fromResponse($response); - } - - return $response; - } - - /** - * Attach a container to a callback to read logs - * - * @param \Docker\Container $container Container to attach - * - * Where $streamType will be 0 for STDIN, 1 for STDOUT, 2 for STDERR and $output will be the string of log - * - * @param callable $callback Callback to attach - * @param boolean $logs Get the backlog of this container - * @param boolean $stream Stream the response - * @param boolean $stdin Get stdin log - * @param boolean $stdout Get stdout log - * @param boolean $stderr Get stderr log - * @param integer $timeout Timeout when - * - * @throws \Docker\Exception\UnexpectedStatusCodeException - * - * @return \GuzzleHttp\Message\ResponseInterface - */ - public function attach(Container $container, callable $callback, $logs = true, $stream = true, $stdin = true, $stdout = true, $stderr = true, $timeout = null) - { - $response = $this->client->post(['/containers/{id}/attach{?data*}', [ - 'id' => $container->getId(), - 'data' => [ - 'logs' => $logs, - 'stream' => $stream, - 'stdin' => $stdin, - 'stdout' => $stdout, - 'stderr' => $stderr, - ] - ]], [ - 'timeout' => $timeout !== null ? $timeout : $this->client->getDefaultOption('timeout'), - 'callback' => $callback, - ]); - - if ($response->getStatusCode() !== "200") { - throw UnexpectedStatusCodeException::fromResponse($response); - } - - return $response; - } - - /** - * Interact with a container - * - * Create a websocket connection which allows to send data on stdin - * - * @param Container $container - * @param boolean $logs Get the backlog of this container - * @param boolean $stream Stream the response - * @param boolean $stdin Get stdin log - * @param boolean $stdout Get stdout log - * @param boolean $stderr Get stderr log - * - * @return InteractiveStream - */ - public function interact(Container $container, $logs = true, $stream = true, $stdin = true, $stdout = true, $stderr = true) - { - $response = $this->client->get(['/containers/{id}/attach/ws{?data*}', [ - 'id' => $container->getId(), - 'data' => [ - 'logs' => $logs, - 'stream' => $stream, - 'stdin' => $stdin, - 'stdout' => $stdout, - 'stderr' => $stderr, - ] - ]], [ - 'headers' => [ - 'Origin' => 'php://docker-php', - 'Upgrade' => 'websocket', - 'Connection' => 'Upgrade', - 'Sec-WebSocket-Version' => '13', - 'Sec-WebSocket-Key' => base64_encode(uniqid()), - ], - ]); - - return new InteractiveStream($response->getBody()); - } - - /** - * Wait for a container to finish - * - * @param \Docker\Container $container - * @param integer|null $timeout - * - * @throws \Docker\Exception\UnexpectedStatusCodeException - * - * @return \Docker\Manager\ContainerManager - */ - public function wait(Container $container, $timeout = null) - { - $response = $this->client->post(['/containers/{id}/wait', [ - 'id' => $container->getId() - ]], [ - 'timeout' => null === $timeout ? $this->client->getDefaultOption('timeout') : $timeout, - ]); - - if ($response->getStatusCode() !== "200") { - throw UnexpectedStatusCodeException::fromResponse($response); - } - - $container->setExitCode($response->json()['StatusCode']); - - $this->inspect($container); - - return $this; - } - - /** - * Stop a running container - * - * @param \Docker\Container $container - * @param integer $timeout - * - * @throws \Docker\Exception\UnexpectedStatusCodeException - * - * @return \Docker\Manager\ContainerManager - */ - public function stop(Container $container, $timeout = 5) - { - $response = $this->client->post(['/containers/{id}/stop?t={timeout}', [ - 'id' => $container->getId(), - 'timeout' => $timeout, - ]],[ - 'wait' => true, - ]); - - if ($response->getStatusCode() !== "204" && $response->getStatusCode() !== "304") { - throw UnexpectedStatusCodeException::fromResponse($response); - } - - $this->inspect($container); - - return $this; - } - - /** - * Remove a container from docker server - * - * @param \Docker\Container $container - * @param boolean $volumes - * - * @throws \Docker\Exception\UnexpectedStatusCodeException - * - * @return \Docker\Manager\ContainerManager - */ - public function remove(Container $container, $volumes = false) - { - $response = $this->client->delete(['/containers/{id}?v={volumes}', [ - 'id' => $container->getId(), - 'volumes' => (integer)$volumes, - // TODO: implement force option - ]],[ - 'wait' => true - ]); - - if ($response->getStatusCode() !== "204") { - throw UnexpectedStatusCodeException::fromResponse($response); - } - - return $this; - } - - /** - * Remove multiple containers from docker server - * - * @param \Docker\Container[]|array $containers - * @param boolean $volumes - * - * @throws \Docker\Exception\UnexpectedStatusCodeException - * - * @return \Docker\Manager\ContainerManager - */ - public function removeContainers(array $containers, $volumes = false) - { - foreach ($containers as $container) { - if (!$container instanceof Container) { - $containerId = $container; - - $container = new Container(); - $container->setId($containerId); - } - - $this->remove($container, $volumes); - } - - return $this; - } - - /** - * List process running inside a container - * - * @param Container $container - * @param string $psArgs - * - * @throws \Docker\Exception\UnexpectedStatusCodeException - * - * @return array - */ - public function top(Container $container, $psArgs = "aux") - { - $response = $this->client->get(['/containers/{id}/top?ps_args={ps_args}', [ - 'id' => $container->getId(), - 'ps_args' => $psArgs - ]]); - - if ($response->getStatusCode() !== "200") { - throw UnexpectedStatusCodeException::fromResponse($response); - } - - $processes = []; - $data = $response->json(); - - $keys = $data['Titles']; - - foreach ($data['Processes'] as $values) { - $processes[] = array_combine($keys, $values); - } - - return $processes; - } - - /** - * Get changes on a container filesystem - * - * @param Container $container - * - * @throws \Docker\Exception\UnexpectedStatusCodeException - * - * @return array - */ - public function changes(Container $container) - { - $response = $this->client->get(['/containers/{id}/changes', [ - 'id' => $container->getId() - ]]); - - if ($response->getStatusCode() !== "200") { - throw UnexpectedStatusCodeException::fromResponse($response); - } - - return $response->json(); - } - - /** - * Export a container to a tar - * - * @param Container $container - * - * @throws \Docker\Exception\UnexpectedStatusCodeException - * - * @return \GuzzleHttp\Stream\Stream - */ - public function export(Container $container) - { - $response = $this->client->get(['/containers/{id}/export', [ - 'id' => $container->getId() - ]]); - - if ($response->getStatusCode() !== "200") { - throw UnexpectedStatusCodeException::fromResponse($response); - } - - return $response->getBody(); - } - - /** - * Get logs from a container - * - * @param Container $container - * @param bool $follow - * @param bool $stdout - * @param bool $stderr - * @param bool $timestamp - * @param string $tail - * - * @return array - */ - public function logs(Container $container, $follow = false, $stdout = false, $stderr = false, $timestamp = false, $tail = "all") - { - $logs = []; - - $callback = function ($output, $type) use(&$logs) { - $logs[] = ['type' => $type, 'output' => $output]; - }; - - $this->client->get(['/containers/{id}/logs{?data*}', [ - 'id' => $container->getId(), - 'data' => [ - 'follow' => (int)$follow, - 'stdout' => (int)$stdout, - 'stderr' => (int)$stderr, - 'timestamps' => (int)$timestamp, - 'tail' => $tail, - ], - ]], [ - 'callback' => $callback, - 'wait' => true, - ]); - - return $logs; - } - - /** - * Restart a container - * - * @param Container $container - * @param integer $timeBeforeKill number of seconds to wait before killing the container - * - * @throws \Docker\Exception\UnexpectedStatusCodeException - */ - public function restart(Container $container, $timeBeforeKill = 5) - { - $response = $this->client->post(['/containers/{id}/restart?t={time}', [ - 'id' => $container->getId(), - 'time' => $timeBeforeKill - ]]); - - if ($response->getStatusCode() !== "204") { - throw UnexpectedStatusCodeException::fromResponse($response); - } - } - - /** - * Send a signal to container - * - * @param Container $container - * @param string $signal - * - * @throws \Docker\Exception\UnexpectedStatusCodeException - */ - public function kill(Container $container, $signal = "SIGKILL") - { - $response = $this->client->post(['/containers/{id}/kill?signal={signal}', [ - 'id' => $container->getId(), - 'signal' => $signal - ]]); - - if ($response->getStatusCode() !== "204") { - throw UnexpectedStatusCodeException::fromResponse($response); - } - } - - /** - * Rename a container (API v1.17) - * - * @param Container $container - * @param string $newname - * - * @throws \Docker\Exception\UnexpectedStatusCodeException - */ - public function rename(Container $container, $newname) - { - $response = $this->client->post(['/containers/{id}/rename?name={newname}', [ - 'id' => $container->getId(), - 'newname' => $newname - ]]); - - if ($response->getStatusCode() !== "204") { - throw UnexpectedStatusCodeException::fromResponse($response); - } - - return $this; - } - -} diff --git a/src/Docker/Manager/ImageManager.php b/src/Docker/Manager/ImageManager.php deleted file mode 100644 index 76932358..00000000 --- a/src/Docker/Manager/ImageManager.php +++ /dev/null @@ -1,315 +0,0 @@ -client = $client; - } - - /** - * Get all images from docker daemon - * - * @param boolean $dangling Filter dangling images - * @param boolean $all List all images including untagged - * - * @throws \Docker\Exception\UnexpectedStatusCodeException - * - * @return Image[] - */ - public function findAll($dangling = false, $all = false) - { - $params = []; - - if ($all) { - $params['all'] = 1; - } - - if ($dangling) { - $params['dangling'] = 1; - } - - /** @var Response $response */ - $response = $this->client->get('/images/json', [ - 'query' => $params - ]); - - if ($response->getStatusCode() !== "200") { - throw UnexpectedStatusCodeException::fromResponse($response); - } - - $images = $response->json(); - - if (!is_array($images)) { - return []; - } - - $coll = []; - - foreach ($images as $data) { - $image = new Image(); - $image->setId($data['Id']); - - foreach ($data['RepoTags'] as $repoTag) { - list($repository, $tag) = explode(':', $repoTag); - - $tagImage = clone $image; - $tagImage->setRepository($repository); - $tagImage->setTag($tag); - - $coll[] = $tagImage; - } - } - - return $coll; - } - - /** - * Get an image from docker daemon - * - * @param string $repository Name of image to get - * @param string $tag Tag of the image to get (default "latest") - * - * @return Image - */ - public function find($repository, $tag = 'latest') - { - $image = new Image($repository, $tag); - - $data = $this->inspect($image); - $image->setId($data['Id']); - - return $image; - } - - /** - * Inspect an image - * - * @param \Docker\Image $image - * - * @throws \Docker\Exception\ImageNotFoundException - * @throws \Docker\Exception\UnexpectedStatusCodeException - * @throws \GuzzleHttp\Exception\RequestException - * - * @return array json data from docker inspect - */ - public function inspect(Image $image) - { - try { - # Images need not have a name and tag,(__toString() may return ':') - # so prefer an id hash as the key - if (null != $image->getId()) { - $id = $image->getId(); - } else { - $id = $image->__toString(); - } - - $response = $this->client->get(['/images/{id}/json', ['id' => $id]]); - - } catch (RequestException $e) { - if ($e->hasResponse() && $e->getResponse()->getStatusCode() == "404") { - throw new ImageNotFoundException($id, $e); - } - - throw $e; - } - - if ($response->getStatusCode() !== "200") { - throw UnexpectedStatusCodeException::fromResponse($response); - } - - return $response->json(); - } - - /** - * Pull an image from registry - * - * @param string $name Name of image to pull - * @param string $tag Tag of image - * @param callable $callback Callback to retrieve log of pull - * - * @throws \Docker\Exception\UnexpectedStatusCodeException - * - * @return Image - */ - public function pull($name, $tag = 'latest', callable $callback = null) - { - if (null === $callback) { - $callback = function () {}; - } - - $response = $this->client->post(['/images/create?fromImage={image}&tag={tag}', ['image' => $name, 'tag' => $tag]], [ - 'callback' => $callback, - 'wait' => true, - ]); - - if ($response->getStatusCode() !== "200") { - throw UnexpectedStatusCodeException::fromResponse($response); - } - - $image = new Image($name, $tag); - $data = $this->inspect($image); - - if (!$image->getId()) { - $image->setId($data['Id']); - } - - return $image; - } - - /** - * Remove an image from docker daemon - * - * @param Image $image Image to remove - * @param boolean $force Force removal of image (default false) - * @param boolean $noprune Do not remove parent images (default false) - * - * @throws \Docker\Exception\UnexpectedStatusCodeException - * - * @return ImageManager - */ - public function remove(Image $image, $force = false, $noprune = false) - { - $response = $this->client->delete(['/images/{image}?force={force}&noprune={noprune}', [ - 'image' => $image->__toString(), - 'force' => $force, - 'noprune' => $noprune, - 'wait' => true - ]]); - - if ($response->getStatusCode() !== "200") { - throw UnexpectedStatusCodeException::fromResponse($response); - } - - return $this; - } - - /** - * Remove multiple images from docker daemon - * - * @param Image[]|array $images Images to remove - * @param boolean $force Force removal of image (default false) - * @param boolean $noprune Do not remove parent images (default false) - * - * @throws \Docker\Exception\UnexpectedStatusCodeException - * - * @return ImageManager - */ - public function removeImages(array $images, $force = false, $noprune = false) - { - foreach ($images as $image) { - if (!$image instanceof Image) { - $imageId = $image; - - $image = new Image(); - $image->setId($imageId); - } - - $this->remove($image, $force, $noprune); - } - - return $this; - } - - /** - * Search for an image on Docker Hub. - * - * @param string $term term to search - * - * @throws \Docker\Exception\UnexpectedStatusCodeException - * - * @return array - */ - public function search($term) - { - $response = $this->client->get( - [ - '/images/search?term={term}', - [ - 'term' => $term, - ] - ] - ); - - if ($response->getStatusCode() !== "200") { - throw UnexpectedStatusCodeException::fromResponse($response); - } - - return $response->json(); - } - - /** - * Tag an image - * - * @param Image $image image to tag - * @param $repository Repository name to use - * @param string $tag Tag to use - * @param bool $force Force to set tag even if an image with the same name already exists ? - * - * @throws \Docker\Exception\UnexpectedStatusCodeException - * - * @return ImageManager - */ - public function tag(Image $image, $repository, $tag = 'latest', $force = false) - { - $response = $this->client->post([ - '/images/{name}/tag?repo={repository}&tag={tag}&force={force}', [ - 'name' => $image->getId(), - 'repository' => $repository, - 'tag' => $tag, - 'force' => intval($force) - ] - ]); - - if ($response->getStatusCode() !== "201") { - throw UnexpectedStatusCodeException::fromResponse($response); - } - - $image->setRepository($repository); - $image->setTag($tag); - - return $this; - } - - /** - * Get history of an image - * - * @param Image $image - * - * @throws \Docker\Exception\UnexpectedStatusCodeException - * - * @return array - */ - public function history(Image $image) - { - $response = $this->client->get(['/images/{name}/history', [ - 'name' => $image->__toString() - ]]); - - if ($response->getStatusCode() !== "200") { - throw UnexpectedStatusCodeException::fromResponse($response); - } - - return $response->json(); - } -} diff --git a/src/Docker/Port.php b/src/Docker/Port.php deleted file mode 100644 index 5ca789cb..00000000 --- a/src/Docker/Port.php +++ /dev/null @@ -1,129 +0,0 @@ - $value) { - $this->$key = $value; - } - } - - /** - * @return integer - */ - public function getPort() - { - return $this->port; - } - - /** - * @return string - */ - public function getProtocol() - { - return $this->protocol; - } - - /** - * @return integer - */ - public function getHostPort() - { - return $this->hostPort; - } - - /** - * @return string - */ - public function getHostIp() - { - return $this->hostIp; - } - - /** - * @return array - */ - public function toSpec() - { - return [ - $this->port.'/'.$this->protocol => [ - [ - 'HostIp' => $this->hostIp, - 'HostPort' => (string) $this->hostPort, - ], - ] - ]; - } - - /** - * @return array - */ - public function toExposedPorts() - { - return [$this->port.'/'.$this->protocol => []]; - } - - /** - * [[hostIp:][hostPort]:]port[/protocol] - * - * @param string $raw - * - * @throws \Docker\Exception When port specification is invalid - * - * @return array - */ - public static function parse($raw) - { - if (!preg_match('/(?:(?[0-9\.]{7,15}):)?(?:(?\d{1,5}|):)?(?\d{1,5})(?:\/(?\w+))?/', $raw, $matches)) { - throw new Exception('Invalid port specification "'.$raw.'"'); - } - - $parsed = []; - - foreach (['hostIp', 'hostPort', 'port', 'protocol'] as $key) { - if (array_key_exists($key, $matches)) { - $parsed[$key] = strlen($matches[$key]) > 0 - ? (is_numeric($matches[$key]) - ? (integer) $matches[$key] - : $matches[$key]) - : null; - } else { - $parsed[$key] = null; - } - } - - return $parsed; - } -} diff --git a/src/Docker/PortCollection.php b/src/Docker/PortCollection.php deleted file mode 100644 index ec8f645e..00000000 --- a/src/Docker/PortCollection.php +++ /dev/null @@ -1,84 +0,0 @@ -add($port); - } elseif (is_string($port) || is_integer($port)) { - $this->add(new Port($port)); - } else { - throw new Exception('Invalid port definition "('.gettype($port).') '.var_export($port, true).'"'); - } - } - } - - /** - * @return array - */ - public function toSpec() - { - $spec = []; - - foreach ($this->ports as $port) { - $spec = array_merge($spec, $port->toSpec()); - } - - return $spec; - } - - /** - * @return array - */ - public function toExposedPorts() - { - $exposed = []; - - foreach ($this->ports as $port) { - $exposed = array_merge($exposed, $port->toExposedPorts()); - } - - return $exposed; - } - - /** - * Add a port - * - * @param \Docker\Port $port Port to add - * - * @return PortCollection - */ - public function add(Port $port) - { - $this->ports[] = $port; - - return $this; - } - - /** - * @return Port[] - */ - public function all() - { - return $this->ports; - } -} diff --git a/src/Docker/PortSpecInterface.php b/src/Docker/PortSpecInterface.php deleted file mode 100644 index bf93c4f1..00000000 --- a/src/Docker/PortSpecInterface.php +++ /dev/null @@ -1,19 +0,0 @@ -addEnv(['FOO=BAR']); - - $this->assertEquals(['FOO=BAR'], $container->getEnv()); - } - - public function testAddEnvWithExistingEnv() - { - $container = new Container(['Env' => ['FOO=BAR']]); - $container->addEnv(['BAR=FOO']); - - $this->assertEquals(['FOO=BAR', 'BAR=FOO'], $container->getEnv()); - } - - public function testContainerName() - { - $container = new Container(); - $container->setName('Foobar'); - - $this->assertEquals('Foobar', $container->getName()); - } - - public function testValidContainerName() - { - $container = new Container(); - $container->setName('/Foobar'); - $this->assertEquals('/Foobar', $container->getName()); - } - - public function testInvalidContainerNameOne() - { - $container = new Container(); - $this->setExpectedException('Exception', 'Name was not correctly formatted.'); - $container->setName('Foo/Bar/Baz'); - } - - public function testInvalidContainerNameTwo() - { - $container = new Container(); - $this->setExpectedException('Exception', 'Name was not correctly formatted.'); - $container->setName('Foo!'); - } -} diff --git a/src/Docker/Tests/Context/ContextBuilderTest.php b/src/Docker/Tests/Context/ContextBuilderTest.php deleted file mode 100644 index a70b8b4c..00000000 --- a/src/Docker/Tests/Context/ContextBuilderTest.php +++ /dev/null @@ -1,99 +0,0 @@ -getContext(); - - $this->assertFileExists($context->getDirectory().'/Dockerfile'); - - unset($contextBuilder); - - $this->assertFileNotExists($context->getDirectory().'/Dockerfile'); - } - - public function testWritesContextToDisk() - { - $contextBuilder = new ContextBuilder(); - $context = $contextBuilder->getContext(); - - $this->assertFileExists($context->getDirectory().'/Dockerfile'); - } - - public function testHasDefaultFrom() - { - $contextBuilder = new ContextBuilder(); - $context = $contextBuilder->getContext(); - - $this->assertStringEqualsFile($context->getDirectory().'/Dockerfile', 'FROM base'); - } - - public function testUsesCustomFrom() - { - $contextBuilder = new ContextBuilder(); - $contextBuilder->from('ubuntu:precise'); - - $context = $contextBuilder->getContext(); - - $this->assertStringEqualsFile($context->getDirectory().'/Dockerfile', 'FROM ubuntu:precise'); - } - - public function testCreatesTmpDirectory() - { - $contextBuilder = new ContextBuilder(); - $context = $contextBuilder->getContext(); - - $this->assertFileExists($context->getDirectory()); - } - - public function testWriteTmpFiles() - { - $contextBuilder = new ContextBuilder(); - $contextBuilder->add('/foo', 'random content'); - - $context = $contextBuilder->getContext(); - $filename = preg_replace(<<getDockerfileContent()); - - $this->assertStringEqualsFile($context->getDirectory().'/'.$filename, 'random content'); - } - - public function testWritesAddCommands() - { - $contextBuilder = new ContextBuilder(); - $contextBuilder->add('/foo', 'foo file content'); - - $context = $contextBuilder->getContext(); - - $this->assertRegExp(<<getDockerfileContent() - ); - } - - public function testWritesRunCommands() - { - $contextBuilder = new ContextBuilder(); - $contextBuilder->run('foo command'); - - $context = $contextBuilder->getContext(); - - $this->assertStringEqualsFile($context->getDirectory().'/Dockerfile', <<run(); - - $this->assertEquals(strlen($process->getOutput()), strlen($context->toTar())); - } - - public function testReturnsValidTarStream() - { - $directory = __DIR__.DIRECTORY_SEPARATOR."context-test"; - - $context = new Context($directory); - $this->assertInternalType('resource', $context->toStream()); - } -} diff --git a/src/Docker/Tests/DockerTest.php b/src/Docker/Tests/DockerTest.php deleted file mode 100644 index 1812d513..00000000 --- a/src/Docker/Tests/DockerTest.php +++ /dev/null @@ -1,73 +0,0 @@ -from('ubuntu:precise'); - $contextBuilder->add('/test', 'test file content'); - - $docker = $this->getDocker(); - $content = ""; - - $response = $docker->build($contextBuilder->getContext(), 'foo', function ($output) use (&$content) { - if (isset($output['stream'])) { - $content .= $output['stream']; - } - }); - - $this->assertRegExp('/Successfully built/', $content); - } - - public function testBuildWithExistingDirectory() - { - $docker = $this->getDocker(); - $directory = __DIR__.DIRECTORY_SEPARATOR."Context".DIRECTORY_SEPARATOR."context-test"; - $context = new Context($directory); - $timecalled = 0; - - $docker->build($context, 'foo', function ($output) use (&$content, &$timecalled) { - if (isset($output['stream'])) { - $content .= $output['stream']; - } - $timecalled++; - }); - - $this->assertRegExp('/Successfully built/', $content); - $this->assertGreaterThan(1, $timecalled); - } - - public function testCommit() - { - $container = new Container(); - $container->setImage('ubuntu:precise'); - $container->setCmd(['/bin/true']); - - $docker = $this->getDocker(); - $manager = $docker->getContainerManager(); - - $manager->run($container); - $manager->wait($container); - - $image = $docker->commit($container, ['repo' => 'test', 'tag' => 'foo']); - - $this->assertNotEmpty($image->getId()); - $this->assertEquals('test', $image->getRepository()); - $this->assertEquals('foo', $image->getTag()); - } - - public function testGetContainerManager() - { - $docker = $this->getDocker(); - - $this->assertInstanceOf('Docker\\Manager\\ContainerManager', $docker->getContainerManager()); - } -} diff --git a/src/Docker/Tests/Manager/ContainerManagerTest.php b/src/Docker/Tests/Manager/ContainerManagerTest.php deleted file mode 100644 index 69cd4282..00000000 --- a/src/Docker/Tests/Manager/ContainerManagerTest.php +++ /dev/null @@ -1,553 +0,0 @@ -getDocker()->getContainerManager(); - } - - public function testFindAll() - { - $container = new Container(['Image' => 'ubuntu:precise', 'Cmd' => ['/bin/sleep', '1']]); - - $manager = $this->getManager(); - $manager->run($container); - - $this->assertInternalType('array', $manager->findAll()); - } - - public function testCreate() - { - $container = new Container(['Image' => 'ubuntu:precise', 'Cmd' => ['/bin/true']]); - - $manager = $this->getManager(); - $manager->create($container); - - $this->assertNotEmpty($container->getId()); - } - - public function testInteract() - { - $container = new Container([ - 'Image' => 'ubuntu:precise', - 'Cmd' => ['/bin/bash'], - 'AttachStdin' => false, - 'AttachStdout' => true, - 'AttachStderr' => true, - 'OpenStdin' => true, - 'Tty' => true, - ]); - - $manager = $this->getManager(); - $manager->create($container); - $stream = $manager->interact($container); - $manager->start($container); - - $this->assertNotEmpty($container->getId()); - $this->assertInstanceOf('\Docker\Http\Stream\InteractiveStream', $stream); - - stream_set_blocking($stream->getSocket(), 0); - - $read = [$stream->getSocket()]; - $write = null; - $expect = null; - - $stream->write("echo test\n"); - $data = ""; - do { - $frame = $stream->receive(true); - $data .= $frame['data']; - } while (stream_select($read, $write, $expect, 1) > 0); - - $manager->stop($container, 1); - - $this->assertRegExp('#root@'.substr($container->getId(), 0, 12).':/\# echo test#', $data, $data); - } - - public function testCreateThrowsRightFormedException() - { - $container = new Container(['Image' => 'non-existent']); - - $manager = $this->getManager(); - - try { - $manager->create($container); - } catch (\GuzzleHttp\Exception\RequestException $e) { - $this->assertTrue($e->hasResponse()); - $this->assertEquals("404", $e->getResponse()->getStatusCode()); - $this->assertContains('No such image: non-existent (tag: latest)', $e->getMessage()); - } - } - - public function testStart() - { - $container = new Container(['Image' => 'ubuntu:precise', 'Cmd' => ['/bin/true']]); - - $manager = $this->getManager(); - $manager->create($container); - $manager->start($container); - - $runtimeInformations = $container->getRuntimeInformations(); - - $this->assertEquals(0, $runtimeInformations['State']['ExitCode']); - } - - public function testRunDefault() - { - $container = new Container(['Image' => 'ubuntu:precise', 'Cmd' => ['/bin/true']]); - $manager = $this - ->getMockBuilder('\Docker\Manager\ContainerManager') - ->setMethods(['create', 'start', 'wait']) - ->disableOriginalConstructor() - ->getMock(); - - $container->setExitCode(0); - - $manager->expects($this->once()) - ->method('create') - ->with($this->isInstanceOf('\Docker\Container')) - ->will($this->returnSelf()); - - $manager->expects($this->once()) - ->method('start') - ->with($this->isInstanceOf('\Docker\Container')) - ->will($this->returnSelf()); - - $manager->expects($this->once()) - ->method('wait') - ->with($this->isInstanceOf('\Docker\Container')) - ->will($this->returnSelf()); - - $this->assertTrue($manager->run($container)); - } - - public function testRunAttach() - { - $container = new Container(['Image' => 'ubuntu:precise', 'Cmd' => ['/bin/true']]); - $manager = $this - ->getMockBuilder('\Docker\Manager\ContainerManager') - ->setMethods(['create', 'start', 'wait', 'attach']) - ->disableOriginalConstructor() - ->getMock(); - - $response = $this->getMockBuilder('\GuzzleHttp\Message\Response')->disableOriginalConstructor()->getMock(); - $stream = $this->getMockBuilder('\GuzzleHttp\Stream\Stream')->disableOriginalConstructor()->getMock(); - - $container->setExitCode(0); - $callback = function () {}; - - $manager->expects($this->once()) - ->method('create') - ->with($this->isInstanceOf('\Docker\Container')) - ->will($this->returnSelf()); - - $manager->expects($this->once()) - ->method('attach') - ->with($this->isInstanceOf('\Docker\Container'), $this->equalTo($callback), $this->equalTo(true), $this->equalTo(true), $this->equalTo(true), $this->equalTo(true), $this->equalTo(true), $this->equalTo(null)) - ->will($this->returnValue($response)); - - $manager->expects($this->once()) - ->method('start') - ->with($this->isInstanceOf('\Docker\Container')) - ->will($this->returnSelf()); - - $response->expects($this->once()) - ->method('getBody') - ->will($this->returnValue($stream)); - - $manager->expects($this->once()) - ->method('wait') - ->with($this->isInstanceOf('\Docker\Container')) - ->will($this->returnSelf()); - - $this->assertTrue($manager->run($container, $callback)); - } - - public function testRunDaemon() - { - $container = new Container(['Image' => 'ubuntu:precise', 'Cmd' => ['/bin/true']]); - $manager = $this - ->getMockBuilder('\Docker\Manager\ContainerManager') - ->setMethods(['create', 'start', 'wait']) - ->disableOriginalConstructor() - ->getMock(); - - $container->setExitCode(0); - - $manager->expects($this->once()) - ->method('create') - ->with($this->isInstanceOf('\Docker\Container')) - ->will($this->returnSelf()); - - $manager->expects($this->once()) - ->method('start') - ->with($this->isInstanceOf('\Docker\Container')) - ->will($this->returnSelf()); - - $manager->expects($this->never()) - ->method('wait'); - - $this->assertNull($manager->run($container, null, [], true)); - } - - public function testAttach() - { - $container = new Container(['Image' => 'ubuntu:precise', 'Cmd' => ['/bin/bash', '-c', 'echo -n "output"']]); - $manager = $this->getManager(); - - $type = 0; - $output = ""; - - $manager->create($container); - $response = $manager->attach($container, function ($log, $stdtype) use (&$type, &$output) { - $type = $stdtype; - $output = $log; - }); - $manager->start($container); - - $response->getBody()->getContents(); - - $this->assertEquals(1, $type); - $this->assertEquals('output', $output); - } - - public function testAttachStderr() - { - $container = new Container(['Image' => 'ubuntu:precise', 'Cmd' => ['/bin/bash', '-c', 'echo -n "error" 1>&2']]); - $manager = $this->getManager(); - - $type = 0; - $output = ""; - - $manager->create($container); - $response = $manager->attach($container, function ($log, $stdtype) use (&$type, &$output) { - $type = $stdtype; - $output = $log; - }); - $manager->start($container); - - $response->getBody()->getContents(); - - $this->assertEquals(2, $type); - $this->assertEquals('error', $output); - } - - /** - * Not sure how to reliably test that we actually waited for the container - * but this should at least ensure no exception is thrown - */ - public function testWait() - { - $container = new Container(['Image' => 'ubuntu:precise', 'Cmd' => ['/bin/sleep', '1']]); - - $manager = $this->getManager(); - $manager->run($container); - $manager->wait($container); - - $runtimeInformations = $container->getRuntimeInformations(); - - $this->assertEquals(0, $runtimeInformations['State']['ExitCode']); - } - - /** - * @expectedException GuzzleHttp\Exception\RequestException - */ - public function testWaitWithTimeout() - { - if (getenv('DOCKER_TLS_VERIFY')) { - $this->markTestSkipped('This test failed when using ssl due to this bug : https://bugs.php.net/bug.php?id=41631'); - } - - $container = new Container(['Image' => 'ubuntu:precise', 'Cmd' => ['/bin/sleep', '2']]); - - $manager = $this->getManager(); - $manager->create($container); - $manager->start($container); - $manager->wait($container, 1); - } - - public function testTimeoutExceptionHasRequest() - { - if (getenv('DOCKER_TLS_VERIFY')) { - $this->markTestSkipped('This test failed when using ssl due to this bug : https://bugs.php.net/bug.php?id=41631'); - } - - $container = new Container(['Image' => 'ubuntu:precise', 'Cmd' => ['/bin/sleep', '2']]); - - $manager = $this->getManager(); - $manager->run($container); - - try { - $manager->wait($container, 1); - } catch (RequestException $e) { - $this->assertInstanceOf('Docker\\Http\\Request', $e->getRequest()); - } - } - - public function testExposeFixedPort() - { - $container = new Container(['Image' => 'ubuntu:precise', 'Cmd' => ['/bin/sleep', '1']]); - - $port = new Port('8888:80/tcp'); - - $container->setExposedPorts($port); - - $manager = $this->getManager(); - $manager->create($container); - $manager->start($container, ['PortBindings' => $port->toSpec()]); - - $this->assertEquals(8888, $container->getMappedPort(80)->getHostPort()); - } - - public function testExposeRandomPort() - { - $container = new Container(['Image' => 'ubuntu:precise', 'Cmd' => ['/bin/sleep', '1']]); - - $port = new Port('80/tcp'); - $container->setExposedPorts($port); - - $manager = $this->getManager(); - $manager->create($container); - $manager->start($container, ['PortBindings' => $port->toSpec()]); - - $this->assertInternalType('integer', $container->getMappedPort(80)->getHostPort()); - } - - public function testInspect() - { - $manager = $this->getManager(); - - $this->assertEquals(null, $manager->find('foo')); - - $container = new Container(['Image' => 'ubuntu:precise', 'Cmd' => ['/bin/true']]); - $manager->create($container); - - $this->assertInstanceOf('Docker\\Container', $manager->find($container->getId())); - } - - public function testRemove() - { - $container = new Container(['Image' => 'ubuntu:precise', 'Cmd' => ['date']]); - - $manager = $this->getManager(); - $manager->create($container); - $manager->start($container); - $manager->wait($container); - $manager->remove($container); - - $this->setExpectedException('\\Docker\\Exception\\ContainerNotFoundException', 'Container not found'); - $manager->inspect($container); - } - - public function testRemoveContainers() - { - $containers = ['3360ea744df2', 'a412d121d015']; - $manager = $this - ->getMockBuilder('\Docker\Manager\ContainerManager') - ->setMethods(['remove']) - ->disableOriginalConstructor() - ->getMock(); - - $manager->expects($this->exactly(2)) - ->method('remove') - ->with($this->isInstanceOf('\Docker\Container'), false) - ->will($this->returnSelf()); - - $manager->removeContainers($containers); - } - - public function testTop() - { - $container = new Container(['Image' => 'ubuntu:precise', 'Cmd' => ['sleep', '2']]); - $manager = $this->getManager(); - $manager->run($container, null, [], true); - - $processes = $manager->top($container); - - $this->assertCount(1, $processes); - $this->assertArrayHasKey('COMMAND', $processes[0]); - $this->assertEquals('sleep 2', $processes[0]['COMMAND']); - - $manager->wait($container); - $manager->remove($container); - } - - public function testChanges() - { - $container = new Container(['Image' => 'ubuntu:precise', 'Cmd' => ['touch', '/docker-php-test']]); - $manager = $this->getManager(); - $manager->run($container); - $manager->wait($container); - - $changes = $manager->changes($container); - - $manager->remove($container); - - $this->assertCount(1, $changes); - $this->assertEquals('/docker-php-test', $changes[0]['Path']); - $this->assertEquals(1, $changes[0]['Kind']); - } - - public function testExport() - { - $container = new Container(['Image' => 'ubuntu:precise', 'Cmd' => ['touch', '/docker-php-test']]); - $manager = $this->getManager(); - $manager->run($container); - $manager->wait($container); - - $exportStream = $manager->export($container); - - $this->assertInstanceOf('\GuzzleHttp\Stream\Stream', $exportStream); - - $tarFileName = tempnam(sys_get_temp_dir(), 'docker-php-export-test-'); - $tarFile = fopen($tarFileName, 'w+'); - - stream_copy_to_stream($exportStream->detach(), $tarFile); - fclose($tarFile); - - exec('/usr/bin/env tar -tf '.$tarFileName, $output); - - $this->assertContains('docker-php-test', $output); - $this->assertContains('.dockerinit', $output); - - unlink($tarFileName); - $manager->remove($container); - } - - public function testLogs() - { - $container = new Container(['Image' => 'ubuntu:precise', 'Cmd' => ['echo', 'test']]); - $manager = $this->getManager(); - $manager->run($container); - $manager->stop($container); - $logs = $manager->logs($container, false, true); - $manager->remove($container); - - $this->assertGreaterThanOrEqual(1, count($logs)); - - $logs = array_map(function ($value) { - return $value['output']; - }, $logs); - - $this->assertContains("test", implode("", $logs)); - } - - public function testRestart() - { - $manager = $this->getManager(); - $dockerFileBuilder = new ContextBuilder(); - $dockerFileBuilder->from('ubuntu:precise'); - $dockerFileBuilder->add('/daemon.sh', file_get_contents(__DIR__ . DIRECTORY_SEPARATOR . 'script' . DIRECTORY_SEPARATOR . 'daemon.sh')); - $dockerFileBuilder->run('chmod +x /daemon.sh'); - - $this->getDocker()->build($dockerFileBuilder->getContext(), 'docker-php-restart-test', null, true, false, true); - - $container = new Container(['Image' => 'docker-php-restart-test', 'Cmd' => ['/daemon.sh']]); - $manager->create($container); - $manager->start($container); - $manager->restart($container); - - $logs = $manager->logs($container, false, true); - $logs = array_map(function ($value) { - return $value['output']; - }, $logs); - $processes = $manager->top($container); - - $manager->stop($container); - $manager->remove($container); - - $this->getDocker()->getImageManager()->remove($container->getImage()); - - $this->assertCount(2, $processes); - $this->assertContains('test', implode("", $logs)); - } - - public function testKill() - { - $manager = $this->getManager(); - $dockerFileBuilder = new ContextBuilder(); - $dockerFileBuilder->from('ubuntu:precise'); - $dockerFileBuilder->add('/kill.sh', file_get_contents(__DIR__ . DIRECTORY_SEPARATOR . 'script' . DIRECTORY_SEPARATOR . 'kill.sh')); - $dockerFileBuilder->run('chmod +x /kill.sh'); - - $this->getDocker()->build($dockerFileBuilder->getContext(), 'docker-php-kill-test', null, true, false, true); - - $container = new Container(['Image' => 'docker-php-kill-test', 'Cmd' => ['/kill.sh']]); - $manager->create($container); - $manager->start($container); - $manager->kill($container, "SIGHUP"); - $manager->wait($container); - - $logs = $manager->logs($container, false, true); - $logs = array_map(function ($value) { - return $value['output']; - }, $logs); - - $manager->remove($container); - $this->getDocker()->getImageManager()->remove($container->getImage()); - - $this->assertContains('HUP', implode("", $logs)); - } - - public function testExec() - { - $manager = $this->getManager(); - $dockerFileBuilder = new ContextBuilder(); - $dockerFileBuilder->from('ubuntu:precise'); - $dockerFileBuilder->add('/daemon.sh', file_get_contents(__DIR__ . DIRECTORY_SEPARATOR . 'script' . DIRECTORY_SEPARATOR . 'daemon.sh')); - $dockerFileBuilder->run('chmod +x /daemon.sh'); - - $this->getDocker()->build($dockerFileBuilder->getContext(), 'docker-php-restart-test', null, true, false, true); - - $container = new Container(['Image' => 'docker-php-restart-test', 'Cmd' => ['/daemon.sh']]); - $manager->create($container); - $manager->start($container); - - $type = 0; - $output = ""; - $execId = $manager->exec($container, ['/bin/bash', '-c', 'echo -n "output"']); - - $this->assertNotNull($execId); - - $response = $manager->execstart($execId, function ($log, $stdtype) use (&$type, &$output) { - $type = $stdtype; - $output = $log; - }); - - $response->getBody()->getContents(); - $manager->kill($container); - - $this->assertEquals(1, $type); - $this->assertEquals('output', $output); - } - - public function testRename() - { - $container = new Container(['Image' => 'ubuntu:precise', 'Cmd' => ['/bin/true']]); - - $manager = $this->getManager(); - $manager->create($container); - $manager->start($container); - $manager->rename($container, 'FoobarRenamed'); - - $runtimeInformations = $container->getRuntimeInformations(); - - $this->assertInstanceOf('Docker\\Container', $manager->find('FoobarRenamed')); - $manager->stop($container); // cleanup - $manager->remove($container); - } -} diff --git a/src/Docker/Tests/Manager/ImageManagerTest.php b/src/Docker/Tests/Manager/ImageManagerTest.php deleted file mode 100644 index 7bc42483..00000000 --- a/src/Docker/Tests/Manager/ImageManagerTest.php +++ /dev/null @@ -1,138 +0,0 @@ -getDocker()->getImageManager(); - } - - public function testFind() - { - $manager = $this->getManager(); - $image = $manager->find('test', 'foo'); - - $this->assertEquals('test', $image->getRepository()); - $this->assertEquals('foo', $image->getTag()); - $this->assertNotNull($image->getId()); - } - - public function testFindInexistant() - { - $manager = $this->getManager(); - - $this->setExpectedException('\\Docker\\Exception\\ImageNotFoundException', 'Image not found'); - $manager->find('test'); - } - - public function testPull() - { - $manager = $this->getManager(); - $image = $manager->pull('ubuntu', 'vivid'); - - $this->assertEquals('ubuntu', $image->getRepository()); - $this->assertEquals('vivid', $image->getTag()); - $this->assertNotNull($image->getId()); - } - - public function testFindAll() - { - $manager = $this->getManager(); - - $this->assertInternalType('array', $manager->findAll()); - $this->assertGreaterThanOrEqual(1, count($manager->findAll())); - } - - public function testFindAllAll() - { - $manager = $this->getManager(); - - $images1 = $manager->findAll(); - $images2 = $manager->findAll(false, true); - - $this->assertInternalType('array', $images2); - $this->assertGreaterThan(count($images1), count($images2)); - } - - public function testFindAllDangling() - { - $manager = $this->getManager(); - - $images = $manager->findAll(true); - - $this->assertInternalType('array', $images); - $this->assertGreaterThanOrEqual(1, count($images)); - } - - public function testRemove() - { - $manager = $this->getManager(); - - $image = $manager->find('ubuntu', 'vivid'); - $manager->remove($image, true); - - $this->setExpectedException('\\Docker\\Exception\\ImageNotFoundException', 'Image not found'); - $manager->inspect($image); - } - - public function testRemoveImages() - { - $containers = ['ubuntu:precise', '69c02692b0c1']; - $manager = $this - ->getMockBuilder('\Docker\Manager\ImageManager') - ->setMethods(['remove']) - ->disableOriginalConstructor() - ->getMock(); - - $manager->expects($this->exactly(2)) - ->method('remove') - ->with($this->isInstanceOf('\Docker\Image'), false, false) - ->will($this->returnSelf()); - - $manager->removeImages($containers); - } - - public function testSearch() - { - $manager = $this->getManager(); - - $result = $manager->search('test-image-not-exist'); - $this->assertEmpty($result); - - $this->setExpectedException('\\Docker\\Exception\\APIException', 'Invalid namespace name'); - $manager->search('a/test'); - } - - public function testTag() - { - $image = $this->getManager()->find('test', 'foo'); - - $this->getManager()->tag($image, 'docker-php/unit-test', 'latest'); - - $this->assertEquals('docker-php/unit-test', $image->getRepository()); - $this->assertEquals('latest', $image->getTag()); - - $newImage = $this->getManager()->find('docker-php/unit-test', 'latest'); - $this->assertEquals($image->getId(), $newImage->getId()); - - $this->getManager()->removeImages(array($newImage)); - } - - public function testHistory() - { - $image = $this->getManager()->find('test', 'foo'); - $history = $this->getManager()->history($image); - - $this->assertGreaterThan(1, count($history)); - $this->assertEquals('/bin/true', $history[0]['CreatedBy']); - } -} diff --git a/src/Docker/Tests/PortCollectionTest.php b/src/Docker/Tests/PortCollectionTest.php deleted file mode 100644 index b4729303..00000000 --- a/src/Docker/Tests/PortCollectionTest.php +++ /dev/null @@ -1,36 +0,0 @@ -assertCount(2, $ports->all()); - } - - public function testToSpec() - { - $ports = new PortCollection('80', '22'); - - $this->assertEquals([ - '80/tcp' => [['HostIp' => '', 'HostPort' => '']], - '22/tcp' => [['HostIp' => '', 'HostPort' => '']], - ], $ports->toSpec()); - } - - public function testToExposedPorts() - { - $ports = new PortCollection('80', '22'); - - $this->assertEquals([ - '80/tcp' => [], - '22/tcp' => [], - ], $ports->toExposedPorts()); - } -} diff --git a/src/Docker/Tests/PortTest.php b/src/Docker/Tests/PortTest.php deleted file mode 100644 index 4c1811d8..00000000 --- a/src/Docker/Tests/PortTest.php +++ /dev/null @@ -1,61 +0,0 @@ -assertEquals($parsed, Port::parse($input)); - } - - /** - * @dataProvider provider - */ - public function testToSpec($parsed, $spec, $exposed, $input) - { - $port = new Port($input); - - $this->assertEquals($spec, $port->toSpec()); - } - - /** - * @dataProvider provider - */ - public function testToExposedPorts($parsed, $spec, $exposed, $input) - { - $port = new Port($input); - - $this->assertEquals($exposed, $port->toExposedPorts()); - } - - public function provider() - { - return [ - [ - ['protocol' => 'tcp', 'port' => 80, 'hostIp' => '127.0.0.1', 'hostPort' => 8080], - ['80/tcp' => [['HostIp' => '127.0.0.1', 'HostPort' => 8080]]], - ['80/tcp' => []], - '127.0.0.1:8080:80/tcp', - ], - [ - ['protocol' => null, 'port' => 80, 'hostIp' => '127.0.0.1', 'hostPort' => 8080], - ['80/tcp' => [['HostIp' => '127.0.0.1', 'HostPort' => 8080]]], - ['80/tcp' => []], - '127.0.0.1:8080:80' - ], - [ - ['protocol' => null, 'port' => 80, 'hostIp' => null, 'hostPort' => 8080], - ['80/tcp' => [['HostIp' => '', 'HostPort' => 8080]]], - ['80/tcp' => []], - '8080:80' - ], - ]; - } -} diff --git a/src/Docker/Tests/TestCase.php b/src/Docker/Tests/TestCase.php deleted file mode 100644 index e1e3d973..00000000 --- a/src/Docker/Tests/TestCase.php +++ /dev/null @@ -1,23 +0,0 @@ -docker) { - $this->docker = new Docker($client); - } - - return $this->docker; - } -} diff --git a/src/DockerAsync.php b/src/DockerAsync.php new file mode 100644 index 00000000..111b4efc --- /dev/null +++ b/src/DockerAsync.php @@ -0,0 +1,74 @@ +executeArtaxEndpoint(new SystemEvents($queryParameters), $fetch); + } + + /** + * {@inheritdoc} + */ + public function executeArtaxEndpoint(AmpArtaxEndpoint $endpoint, string $fetch = self::FETCH_OBJECT): Promise + { + return call(function () use ($endpoint, $fetch) { + [$bodyHeaders, $body] = $endpoint->getBody($this->serializer); + $queryString = $endpoint->getQueryString(); + $uri = '' !== $queryString ? $endpoint->getUri().'?'.$queryString : $endpoint->getUri(); + $request = new Request($uri, $endpoint->getMethod()); + $request = $request->withBody($body); + $request = $request->withHeaders($endpoint->getHeaders($bodyHeaders)); + $options = []; + if ($endpoint instanceof ProvideAmpArtaxClientOptions) { + $options = $endpoint->getAmpArtaxClientOptions(); + } + + if ($endpoint instanceof AmpArtaxStreamEndpoint) { + $cancellationTokenSource = new CancellationTokenSource(); + + return $endpoint->parseArtaxStreamResponse( + yield $this->httpClient->request($request, $options, $cancellationTokenSource->getToken()), + $this->serializer, + $cancellationTokenSource, + $fetch + ); + } + + return $endpoint->parseArtaxResponse( + yield $this->httpClient->request($request, $options), + $this->serializer, + $fetch + ); + }); + } +} diff --git a/src/DockerAsyncClient.php b/src/DockerAsyncClient.php new file mode 100644 index 00000000..ca440bd7 --- /dev/null +++ b/src/DockerAsyncClient.php @@ -0,0 +1,87 @@ +resolveOptions($options); + $socketPool = new HttpSocketPool(new StaticSocketPool($options['remote_socket'], new BasicSocketPool())); + $this->client = new DefaultClient(null, $socketPool, $options['ssl']); + } + + public function request($uriOrRequest, array $options = [], CancellationToken $cancellation = null): Promise + { + if ($uriOrRequest instanceof Request) { + $uriOrRequest = $uriOrRequest->withUri('http://localhost'.$uriOrRequest->getUri()); + } + + return $this->client->request($uriOrRequest, $options, $cancellation); + } + + protected function resolveOptions(array $options = []): array + { + $resolver = new OptionsResolver(); + + $resolver->setDefaults([ + 'ssl' => null, + ]); + + $resolver->setRequired([ + 'remote_socket', + ]); + + $resolver->setAllowedTypes('ssl', ['null', ClientTlsContext::class]); + + return $resolver->resolve($options); + } + + public static function createFromEnv(): self + { + $options = [ + 'remote_socket' => \getenv('DOCKER_HOST') ? \getenv('DOCKER_HOST') : 'unix:///var/run/docker.sock', + ]; + + if (\getenv('DOCKER_TLS_VERIFY') && '1' === \getenv('DOCKER_TLS_VERIFY')) { + if (!\getenv('DOCKER_CERT_PATH')) { + throw new \RuntimeException('Connection to docker has been set to use TLS, but no PATH is defined for certificate in DOCKER_CERT_PATH docker environment variable'); + } + + $tlsContext = new ClientTlsContext(); + + $cafile = \getenv('DOCKER_CERT_PATH').DIRECTORY_SEPARATOR.'ca.pem'; + $certfile = \getenv('DOCKER_CERT_PATH').DIRECTORY_SEPARATOR.'cert.pem'; + $keyfile = \getenv('DOCKER_CERT_PATH').DIRECTORY_SEPARATOR.'key.pem'; + + $certificate = new Certificate($certfile, $keyfile); + + $tlsContext = $tlsContext->withCaFile($cafile); + $tlsContext = $tlsContext->withCertificate($certificate); + + if (\getenv('DOCKER_PEER_NAME')) { + $tlsContext = $tlsContext->withPeerName(\getenv('DOCKER_PEER_NAME')); + } + + $options['ssl'] = $tlsContext; + } + + return new static($options); + } +} diff --git a/src/DockerClientFactory.php b/src/DockerClientFactory.php new file mode 100644 index 00000000..918d8724 --- /dev/null +++ b/src/DockerClientFactory.php @@ -0,0 +1,75 @@ +createClient($socketClient, [ + new ContentLengthPlugin(), + new DecoderPlugin(), + new AddHostPlugin(new Uri($host)), + ], [ + 'client_name' => 'docker-client', + ]); + } + + public static function createFromEnv(PluginClientFactory $pluginClientFactory = null): HttpClient + { + $options = [ + 'remote_socket' => \getenv('DOCKER_HOST') ? \getenv('DOCKER_HOST') : 'unix:///var/run/docker.sock', + ]; + + if (\getenv('DOCKER_TLS_VERIFY') && '1' === \getenv('DOCKER_TLS_VERIFY')) { + if (!\getenv('DOCKER_CERT_PATH')) { + throw new \RuntimeException('Connection to docker has been set to use TLS, but no PATH is defined for certificate in DOCKER_CERT_PATH docker environment variable'); + } + + $cafile = \getenv('DOCKER_CERT_PATH').DIRECTORY_SEPARATOR.'ca.pem'; + $certfile = \getenv('DOCKER_CERT_PATH').DIRECTORY_SEPARATOR.'cert.pem'; + $keyfile = \getenv('DOCKER_CERT_PATH').DIRECTORY_SEPARATOR.'key.pem'; + + $stream_context = [ + 'cafile' => $cafile, + 'local_cert' => $certfile, + 'local_pk' => $keyfile, + ]; + + if (\getenv('DOCKER_PEER_NAME')) { + $stream_context['peer_name'] = \getenv('DOCKER_PEER_NAME'); + } + + $options['ssl'] = true; + $options['stream_context_options'] = [ + 'ssl' => $stream_context, + ]; + } + + return self::create($options, $pluginClientFactory); + } +} diff --git a/src/Endpoint/ContainerAttach.php b/src/Endpoint/ContainerAttach.php new file mode 100644 index 00000000..a2d180e9 --- /dev/null +++ b/src/Endpoint/ContainerAttach.php @@ -0,0 +1,32 @@ +getStatusCode() && DockerRawStream::HEADER === $response->getHeaderLine('Content-Type')) { + return new DockerRawStream($response->getBody()); + } + + return $this->transformResponseBody((string) $response->getBody(), $response->getStatusCode(), $serializer); + } + + if (Client::FETCH_RESPONSE === $fetchMode) { + return $response; + } + + throw new InvalidFetchModeException(\sprintf('Fetch mode %s is not supported', $fetchMode)); + } +} diff --git a/src/Endpoint/ContainerAttachWebsocket.php b/src/Endpoint/ContainerAttachWebsocket.php new file mode 100644 index 00000000..fefb3638 --- /dev/null +++ b/src/Endpoint/ContainerAttachWebsocket.php @@ -0,0 +1,44 @@ + 'localhost', + 'Origin' => 'php://docker-php', + 'Upgrade' => 'websocket', + 'Connection' => 'Upgrade', + 'Sec-WebSocket-Version' => '13', + 'Sec-WebSocket-Key' => \base64_encode(\uniqid()), + ]); + } + + public function parsePSR7Response(ResponseInterface $response, SerializerInterface $serializer, string $fetchMode = Client::FETCH_OBJECT) + { + if (Client::FETCH_OBJECT === $fetchMode) { + if (101 === $response->getStatusCode()) { + return new AttachWebsocketStream($response->getBody()); + } + + return $this->transformResponseBody((string) $response->getBody(), $response->getStatusCode(), $serializer); + } + + if (Client::FETCH_RESPONSE === $fetchMode) { + return $response; + } + + throw new InvalidFetchModeException(\sprintf('Fetch mode %s is not supported', $fetchMode)); + } +} diff --git a/src/Endpoint/ContainerLogs.php b/src/Endpoint/ContainerLogs.php new file mode 100644 index 00000000..42335752 --- /dev/null +++ b/src/Endpoint/ContainerLogs.php @@ -0,0 +1,32 @@ +getStatusCode()) { + return new DockerRawStream($response->getBody()); + } + + return $this->transformResponseBody((string) $response->getBody(), $response->getStatusCode(), $serializer); + } + + if (Client::FETCH_RESPONSE === $fetchMode) { + return $response; + } + + throw new InvalidFetchModeException(\sprintf('Fetch mode %s is not supported', $fetchMode)); + } +} diff --git a/src/Endpoint/ExecStart.php b/src/Endpoint/ExecStart.php new file mode 100644 index 00000000..48ddd5c8 --- /dev/null +++ b/src/Endpoint/ExecStart.php @@ -0,0 +1,32 @@ +getStatusCode() && DockerRawStream::HEADER === $response->getHeaderLine('Content-Type')) { + return new DockerRawStream($response->getBody()); + } + + return $this->transformResponseBody((string) $response->getBody(), $response->getStatusCode(), $serializer); + } + + if (Client::FETCH_RESPONSE === $fetchMode) { + return $response; + } + + throw new InvalidFetchModeException(\sprintf('Fetch mode %s is not supported', $fetchMode)); + } +} diff --git a/src/Endpoint/ImageBuild.php b/src/Endpoint/ImageBuild.php new file mode 100644 index 00000000..65d3353f --- /dev/null +++ b/src/Endpoint/ImageBuild.php @@ -0,0 +1,44 @@ +body; + + if (\is_resource($body)) { + $body = new TarStream($body); + } + + return [[], $body]; + } + + public function parsePSR7Response(ResponseInterface $response, SerializerInterface $serializer, string $fetchMode = Client::FETCH_OBJECT) + { + if (Client::FETCH_OBJECT === $fetchMode) { + if (200 === $response->getStatusCode()) { + return new BuildStream($response->getBody(), $serializer); + } + + return $this->transformResponseBody((string) $response->getBody(), $response->getStatusCode(), $serializer); + } + + if (Client::FETCH_RESPONSE === $fetchMode) { + return $response; + } + + throw new InvalidFetchModeException(\sprintf('Fetch mode %s is not supported', $fetchMode)); + } +} diff --git a/src/Endpoint/ImageCreate.php b/src/Endpoint/ImageCreate.php new file mode 100644 index 00000000..196ba3e5 --- /dev/null +++ b/src/Endpoint/ImageCreate.php @@ -0,0 +1,32 @@ +getStatusCode()) { + return new CreateImageStream($response->getBody(), $serializer); + } + + return $this->transformResponseBody((string) $response->getBody(), $response->getStatusCode(), $serializer); + } + + if (Client::FETCH_RESPONSE === $fetchMode) { + return $response; + } + + throw new InvalidFetchModeException(\sprintf('Fetch mode %s is not supported', $fetchMode)); + } +} diff --git a/src/Endpoint/ImagePush.php b/src/Endpoint/ImagePush.php new file mode 100644 index 00000000..3d2eabcc --- /dev/null +++ b/src/Endpoint/ImagePush.php @@ -0,0 +1,37 @@ +name)], '/images/{name}/push'); + } + + public function parsePSR7Response(ResponseInterface $response, SerializerInterface $serializer, string $fetchMode = Client::FETCH_OBJECT) + { + if (Client::FETCH_OBJECT === $fetchMode) { + if (200 === $response->getStatusCode()) { + return new PushStream($response->getBody(), $serializer); + } + + return $this->transformResponseBody((string) $response->getBody(), $response->getStatusCode(), $serializer); + } + + if (Client::FETCH_RESPONSE === $fetchMode) { + return $response; + } + + throw new InvalidFetchModeException(\sprintf('Fetch mode %s is not supported', $fetchMode)); + } +} diff --git a/src/Endpoint/SystemEvents.php b/src/Endpoint/SystemEvents.php new file mode 100644 index 00000000..712e67da --- /dev/null +++ b/src/Endpoint/SystemEvents.php @@ -0,0 +1,43 @@ + 0]; + } + + public function parsePSR7Response(ResponseInterface $response, SerializerInterface $serializer, string $fetchMode = Client::FETCH_OBJECT) + { + if (Client::FETCH_OBJECT === $fetchMode) { + if (200 === $response->getStatusCode()) { + return new EventStream($response->getBody(), $serializer); + } + + return $this->transformResponseBody((string) $response->getBody(), $response->getStatusCode(), $serializer); + } + + if (Client::FETCH_RESPONSE === $fetchMode) { + return $response; + } + + throw new InvalidFetchModeException(\sprintf('Fetch mode %s is not supported', $fetchMode)); + } +} diff --git a/src/Stream/ArtaxCallbackStream.php b/src/Stream/ArtaxCallbackStream.php new file mode 100644 index 00000000..f85d5219 --- /dev/null +++ b/src/Stream/ArtaxCallbackStream.php @@ -0,0 +1,78 @@ +stream = $stream; + $this->cancellationTokenSource = $cancellationTokenSource; + $this->chunkTransformer = $chunkTransformer; + } + + /** + * Called when there is a new frame from the stream. + * + * @param callable $onNewFrame + */ + public function onFrame(callable $onNewFrame): void + { + $this->onNewFrameCallables[] = $onNewFrame; + } + + /** + * Consume stream chunks. + * + * @return Promise + */ + public function listen(): Promise + { + return call(function () { + while (null !== ($chunk = yield $this->stream->read())) { + foreach ($this->onNewFrameCallables as $newFrameCallable) { + $newFrameCallable($this->transformChunk($chunk)); + } + } + }); + } + + /** + * Stop consuming stream chunks. + */ + public function cancel(): void + { + $this->cancellationTokenSource->cancel(); + } + + /** + * Transform stream chunks if required. + * + * @param string $chunk + * + * @return mixed The raw chunk or the transformed chunk + */ + private function transformChunk(string $chunk) + { + if (null === $this->chunkTransformer) { + return $chunk; + } + + return \call_user_func($this->chunkTransformer, $chunk); + } +} diff --git a/src/Stream/AttachWebsocketStream.php b/src/Stream/AttachWebsocketStream.php new file mode 100644 index 00000000..402e417c --- /dev/null +++ b/src/Stream/AttachWebsocketStream.php @@ -0,0 +1,180 @@ +socket = $stream->detach(); + } + + /** + * Send input to the container. + * + * @param string $data Data to send + */ + public function write($data): void + { + $rand = \random_int(0, 28); + $frame = [ + 'fin' => 1, + 'rsv1' => 0, + 'rsv2' => 0, + 'rsv3' => 0, + 'opcode' => 1, // We always send text + 'mask' => 1, + 'len' => \strlen($data), + 'mask_key' => \substr(\md5(\uniqid()), $rand, 4), + 'data' => $data, + ]; + + if (1 === $frame['mask']) { + for ($i = 0; $i < $frame['len']; ++$i) { + $frame['data'][$i] + = \chr(\ord($frame['data'][$i]) ^ \ord($frame['mask_key'][$i % 4])); + } + } + + if ($frame['len'] > 2 ** 16) { + $len = 127; + } elseif ($frame['len'] > 125) { + $len = 126; + } else { + $len = $frame['len']; + } + + $firstByte = ($frame['fin'] << 7) | (($frame['rsv1'] << 7) >> 1) | (($frame['rsv2'] << 7) >> 2) | (($frame['rsv3'] << 7) >> 3) | (($frame['opcode'] << 4) >> 4); + $secondByte = ($frame['mask'] << 7) | (($len << 1) >> 1); + + $this->socketWrite(\chr($firstByte)); + $this->socketWrite(\chr($secondByte)); + + if (126 === $len) { + $this->socketWrite(\pack('n', $frame['len'])); + } elseif (127 === $len) { + $higher = $frame['len'] >> 32; + $lower = ($frame['len'] << 32) >> 32; + $this->socketWrite(\pack('N', $higher)); + $this->socketWrite(\pack('N', $lower)); + } + + if (1 === $frame['mask']) { + $this->socketWrite($frame['mask_key']); + } + + $this->socketWrite($frame['data']); + } + + /** + * Block until it receive a frame from websocket or return null if no more connexion. + * + * @param int $waitTime Time to wait in seconds before return false + * @param int $waitMicroTime Time to wait in microseconds before return false + * @param bool $getFrame Whether to return the frame of websocket or only the data + * + * @return null|false|string|array Null for socket not available, false for no message, string for the last message and the frame array if $getFrame is set to true + */ + public function read($waitTime = 0, $waitMicroTime = 200000, $getFrame = false) + { + if (!\is_resource($this->socket) || \feof($this->socket)) { + return null; + } + + $read = [$this->socket]; + $write = null; + $expect = null; + + if (0 === \stream_select($read, $write, $expect, $waitTime, $waitMicroTime)) { + return false; + } + + $firstByte = $this->socketRead(1); + $frame = []; + $firstByte = \ord($firstByte); + $secondByte = \ord($this->socketRead(1)); + + // First byte decoding + $frame['fin'] = ($firstByte & 128) >> 7; + $frame['rsv1'] = ($firstByte & 64) >> 6; + $frame['rsv2'] = ($firstByte & 32) >> 5; + $frame['rsv3'] = ($firstByte & 16) >> 4; + $frame['opcode'] = ($firstByte & 15); + + // Second byte decoding + $frame['mask'] = ($secondByte & 128) >> 7; + $frame['len'] = ($secondByte & 127); + + // Get length of the frame + if (126 === $frame['len']) { + $frame['len'] = \unpack('n', $this->socketRead(2))[1]; + } elseif (127 === $frame['len']) { + list($higher, $lower) = \array_values(\unpack('N2', $this->socketRead(8))); + $frame['len'] = ($higher << 32) | $lower; + } + + // Get the mask key if needed + if (1 === $frame['mask']) { + $frame['mask_key'] = $this->socketRead(4); + } + + $frame['data'] = $this->socketRead($frame['len']); + + // Decode data if needed + if (1 === $frame['mask']) { + for ($i = 0; $i < $frame['len']; ++$i) { + $frame['data'][$i] = \chr(\ord($frame['data'][$i]) ^ \ord($frame['mask_key'][$i % 4])); + } + } + + if ($getFrame) { + return $frame; + } + + return (string) $frame['data']; + } + + /** + * Force to have something of the expected size (block). + * + * @param $length + * + * @return string + */ + private function socketRead($length) + { + $read = ''; + + do { + $read .= \fread($this->socket, $length - \strlen($read)); + } while (\strlen($read) < $length && !\feof($this->socket)); + + return $read; + } + + /** + * Write to the socket. + * + * @param $data + * + * @return int + */ + private function socketWrite($data) + { + return \fwrite($this->socket, $data); + } +} diff --git a/src/Stream/BuildStream.php b/src/Stream/BuildStream.php new file mode 100644 index 00000000..b6ba9e70 --- /dev/null +++ b/src/Stream/BuildStream.php @@ -0,0 +1,21 @@ +stream = $stream; + } + + /** + * Called when there is a new frame from the stream. + * + * @param callable $onNewFrame + */ + public function onFrame(callable $onNewFrame): void + { + $this->onNewFrameCallables[] = $onNewFrame; + } + + /** + * Read a frame in the stream. + * + * @return mixed + */ + abstract protected function readFrame(); + + /** + * Wait for stream to finish and call callables if defined. + */ + public function wait(): void + { + while (!$this->stream->eof()) { + $frame = $this->readFrame(); + + if (null !== $frame) { + if (!\is_array($frame)) { + $frame = [$frame]; + } + + foreach ($this->onNewFrameCallables as $newFrameCallable) { + \call_user_func_array($newFrameCallable, $frame); + } + } + } + } + + public function closeAndRead(): void + { + $this->stream->close(); + $this->wait(); + } +} diff --git a/src/Stream/CreateImageStream.php b/src/Stream/CreateImageStream.php new file mode 100644 index 00000000..f6cb92ca --- /dev/null +++ b/src/Stream/CreateImageStream.php @@ -0,0 +1,21 @@ +stream = $stream; + } + + /** + * Add a callable to read stdin. + * + * @param callable $callback + */ + public function onStdin(callable $callback): void + { + $this->onStdinCallables[] = $callback; + } + + /** + * Add a callable to read stdout. + * + * @param callable $callback + */ + public function onStdout(callable $callback): void + { + $this->onStdoutCallables[] = $callback; + } + + /** + * Add a callable to read stderr. + * + * @param callable $callback + */ + public function onStderr(callable $callback): void + { + $this->onStderrCallables[] = $callback; + } + + /** + * Read a frame in the stream. + */ + protected function readFrame(): void + { + $header = $this->forceRead(8); + + if (\strlen($header) < 8) { + return; + } + + $decoded = \unpack('C1type/C3/N1size', $header); + $output = $this->forceRead($decoded['size']); + $callbackList = []; + + if (0 === $decoded['type']) { + $callbackList = $this->onStdinCallables; + } + + if (1 === $decoded['type']) { + $callbackList = $this->onStdoutCallables; + } + + if (2 === $decoded['type']) { + $callbackList = $this->onStderrCallables; + } + + foreach ($callbackList as $callback) { + $callback($output); + } + } + + /** + * Force to have something of the expected size (block). + * + * @param $length + * + * @return string + */ + private function forceRead($length) + { + $read = ''; + + do { + $read .= $this->stream->read($length - \strlen($read)); + } while (\strlen($read) < $length && !$this->stream->eof()); + + return $read; + } + + /** + * Wait for stream to finish and call callables if defined. + */ + public function wait(): void + { + while (!$this->stream->eof()) { + $this->readFrame(); + } + } +} diff --git a/src/Stream/EventStream.php b/src/Stream/EventStream.php new file mode 100644 index 00000000..0ba9df6f --- /dev/null +++ b/src/Stream/EventStream.php @@ -0,0 +1,21 @@ +serializer = $serializer; + } + + /** + * {@inheritdoc} + */ + protected function readFrame() + { + $jsonFrameEnd = false; + $lastJsonChar = ''; + $inquote = false; + $jsonFrame = ''; + $level = 0; + + // This is a + while (!$jsonFrameEnd && !$this->stream->eof()) { + $jsonChar = $this->stream->read(1); + + if ('"' === $jsonChar && '\\' !== $lastJsonChar) { + $inquote = !$inquote; + } + + // We ignore white space when it is not part of a quoted string. + if (!$inquote && \in_array($jsonChar, [' ', "\r", "\n", "\t"], true)) { + continue; + } + + if (!$inquote && \in_array($jsonChar, ['{', '['], true)) { + ++$level; + } + + if (!$inquote && \in_array($jsonChar, ['}', ']'], true)) { + --$level; + + if (0 === $level) { + $jsonFrameEnd = true; + $jsonFrame .= $jsonChar; + $lastJsonChar = ''; + continue; + } + } + + $jsonFrame .= $jsonChar; + $lastJsonChar = $jsonChar; + } + + // Invalid last json, or timeout, or connection close before receiving + if (!$jsonFrameEnd) { + return null; + } + + return $this->serializer->deserialize($jsonFrame, 'Docker\\API\\Model\\'.$this->getDecodeClass(), 'json'); + } + + /** + * Get the decode class to pass to serializer. + * + * @return string + */ + abstract protected function getDecodeClass(); +} diff --git a/src/Stream/PushStream.php b/src/Stream/PushStream.php new file mode 100644 index 00000000..c40cb668 --- /dev/null +++ b/src/Stream/PushStream.php @@ -0,0 +1,21 @@ +getContext(); + + $this->assertFileExists($context->getDirectory().'/Dockerfile'); + } + + public function testHasDefaultFrom(): void + { + $contextBuilder = new ContextBuilder(); + $context = $contextBuilder->getContext(); + + $this->assertStringEqualsFile($context->getDirectory().'/Dockerfile', 'FROM base'); + } + + public function testUsesCustomFrom(): void + { + $contextBuilder = new ContextBuilder(); + $contextBuilder->from('ubuntu:precise'); + + $context = $contextBuilder->getContext(); + + $this->assertStringEqualsFile($context->getDirectory().'/Dockerfile', 'FROM ubuntu:precise'); + } + + public function testMultipleFrom(): void + { + $contextBuilder = new ContextBuilder(); + $contextBuilder->from('ubuntu:precise'); + + $contextBuilder->from('test'); + + $content = $contextBuilder->getContext()->getDockerfileContent(); + $this->assertSame("FROM ubuntu:precise\nFROM test", $content); + } + + public function testCreatesTmpDirectory(): void + { + $contextBuilder = new ContextBuilder(); + $context = $contextBuilder->getContext(); + + $this->assertFileExists($context->getDirectory()); + } + + public function testWriteTmpFiles(): void + { + $contextBuilder = new ContextBuilder(); + $contextBuilder->add('/foo', 'random content'); + + $context = $contextBuilder->getContext(); + $filename = \preg_replace(<<getDockerfileContent()); + + $this->assertStringEqualsFile($context->getDirectory().'/'.$filename, 'random content'); + } + + public function testWriteTmpFileFromStream(): void + { + $contextBuilder = new ContextBuilder(); + $stream = \fopen('php://temp', 'r+'); + $this->assertSame(7, \fwrite($stream, 'test123')); + \rewind($stream); + $contextBuilder->addStream('/foo', $stream); + + $context = $contextBuilder->getContext(); + $filename = \preg_replace(<<getDockerfileContent()); + $this->assertStringEqualsFile($context->getDirectory().'/'.$filename, 'test123'); + } + + public function testWriteTmpFileFromDisk(): void + { + $contextBuilder = new ContextBuilder(); + $file = \tempnam('', ''); + \file_put_contents($file, 'abc'); + $this->assertStringEqualsFile($file, 'abc'); + $contextBuilder->addFile('/foo', $file); + + $context = $contextBuilder->getContext(); + $filename = \preg_replace(<<getDockerfileContent()); + $this->assertStringEqualsFile($context->getDirectory().'/'.$filename, 'abc'); + } + + public function testWriteTmpDirFromDisk(): void + { + $contextBuilder = new ContextBuilder(); + $dir = \tempnam(\sys_get_temp_dir(), ''); + \unlink($dir); + \mkdir($dir); + \file_put_contents($dir.'/test', 'abc'); + $this->assertStringEqualsFile($dir.'/test', 'abc'); + $contextBuilder->addFile('/foo', $dir); + + $context = $contextBuilder->getContext(); + $filename = \preg_replace(<<getDockerfileContent()); + $this->assertStringEqualsFile($context->getDirectory().'/'.$filename.'/test', 'abc'); + } + + public function testWritesAddCommands(): void + { + $contextBuilder = new ContextBuilder(); + $contextBuilder->add('/foo', 'foo file content'); + + $context = $contextBuilder->getContext(); + + $this->assertRegExp(<<getDockerfileContent() + ); + } + + public function testWritesRunCommands(): void + { + $contextBuilder = new ContextBuilder(); + $contextBuilder->run('foo command'); + + $context = $contextBuilder->getContext(); + + $this->assertStringEqualsFile($context->getDirectory().'/Dockerfile', <<env('foo', 'bar'); + + $context = $contextBuilder->getContext(); + + $this->assertStringEqualsFile($context->getDirectory().'/Dockerfile', <<copy('/foo', '/bar'); + + $context = $contextBuilder->getContext(); + + $this->assertStringEqualsFile($context->getDirectory().'/Dockerfile', <<workdir('/foo'); + + $context = $contextBuilder->getContext(); + + $this->assertStringEqualsFile($context->getDirectory().'/Dockerfile', <<expose('80'); + + $context = $contextBuilder->getContext(); + + $this->assertStringEqualsFile($context->getDirectory().'/Dockerfile', <<user('user1'); + $content = $contextBuilder->getContext()->getDockerfileContent(); + $this->assertStringEndsWith("\nUSER user1", $content); + + $contextBuilder->user('user2'); + $content = $contextBuilder->getContext()->getDockerfileContent(); + $this->assertStringEndsWith("\nUSER user1\nUSER user2", $content); + } + + public function testWritesVolumeCommands(): void + { + $contextBuilder = new ContextBuilder(); + $contextBuilder->volume('volume1'); + $content = $contextBuilder->getContext()->getDockerfileContent(); + $this->assertStringEndsWith("\nVOLUME volume1", $content); + + $contextBuilder->volume('volume2'); + $content = $contextBuilder->getContext()->getDockerfileContent(); + $this->assertStringEndsWith("\nVOLUME volume1\nVOLUME volume2", $content); + } + + public function testWritesCommandCommand(): void + { + $contextBuilder = new ContextBuilder(); + $contextBuilder->command('test123'); + + $content = $contextBuilder->getContext()->getDockerfileContent(); + $this->assertStringEndsWith("\nCMD test123", $content); + + $contextBuilder->command('changed'); + $content = $contextBuilder->getContext()->getDockerfileContent(); + $this->assertNotContains('CMD test123', $content); + $this->assertStringEndsWith("\nCMD changed", $content); + } + + public function testWritesEntrypointCommand(): void + { + $contextBuilder = new ContextBuilder(); + $contextBuilder->entrypoint('test123'); + + $content = $contextBuilder->getContext()->getDockerfileContent(); + $this->assertStringEndsWith("\nENTRYPOINT test123", $content); + + $contextBuilder->entrypoint('changed'); + $content = $contextBuilder->getContext()->getDockerfileContent(); + $this->assertNotContains('ENTRYPOINT test123', $content); + $this->assertStringEndsWith("\nENTRYPOINT changed", $content); + } + + public function testTar(): void + { + $contextBuilder = new ContextBuilder(); + $contextBuilder->setFormat(Context::FORMAT_TAR); + $context = $contextBuilder->getContext(); + $content = $context->read(); + $this->assertInternalType('string', $content); + $this->assertSame($context->toTar(), $content); + } + + public function testTraverseSymlinks(): void + { + $contextBuilder = new ContextBuilder(); + $dir = \tempnam('', ''); + \unlink($dir); + \mkdir($dir); + $file = $dir.'/test'; + + \file_put_contents($file, 'abc'); + + $linkFile = $file.'-symlink'; + \symlink($file, $linkFile); + + $contextBuilder->addFile('/foo', $dir); + + $context = $contextBuilder->getContext(); + + $filename = \preg_replace(<<getDockerfileContent()); + \unlink($file); + $context->setCleanup(false); + $this->assertStringEqualsFile($context->getDirectory().'/'.$filename.'/test-symlink', 'abc'); + } +} diff --git a/tests/Context/ContextTest.php b/tests/Context/ContextTest.php new file mode 100644 index 00000000..cd2a113f --- /dev/null +++ b/tests/Context/ContextTest.php @@ -0,0 +1,67 @@ +run(); + + $this->assertSame(\strlen($process->getOutput()), \strlen($context->toTar())); + } + + public function testReturnsValidTarStream(): void + { + $directory = __DIR__.DIRECTORY_SEPARATOR.'context-test'; + + $context = new Context($directory); + $this->assertInternalType('resource', $context->toStream()); + } + + public function testDirectorySetter(): void + { + $context = new Context('abc'); + $this->assertSame('abc', $context->getDirectory()); + $context->setDirectory('def'); + $this->assertSame('def', $context->getDirectory()); + } + + /** + * @expectedException \Symfony\Component\Process\Exception\ProcessFailedException + */ + public function testTarFailed(): void + { + $directory = __DIR__.DIRECTORY_SEPARATOR.'context-test'; + $path = \getenv('PATH'); + \putenv('PATH=/'); + $context = new Context($directory); + try { + $context->toTar(); + } finally { + \putenv("PATH=$path"); + } + } + + public function testRemovesFilesOnDestruct(): void + { + $context = (new ContextBuilder())->getContext(); + $file = $context->getDirectory().'/Dockerfile'; + $this->assertFileExists($file); + + unset($context); + + $this->assertFileNotExists($file); + } +} diff --git a/src/Docker/Tests/Context/context-test/Dockerfile b/tests/Context/context-test/Dockerfile similarity index 100% rename from src/Docker/Tests/Context/context-test/Dockerfile rename to tests/Context/context-test/Dockerfile diff --git a/tests/DockerAsyncTest.php b/tests/DockerAsyncTest.php new file mode 100644 index 00000000..29b2f3bf --- /dev/null +++ b/tests/DockerAsyncTest.php @@ -0,0 +1,86 @@ +assertInstanceOf(DockerAsync::class, DockerAsync::create()); + } + + public function testAsync(): void + { + Loop::run(function () { + $docker = DockerAsync::create(); + + $containerConfig = new ContainersCreatePostBody(); + $containerConfig->setImage('busybox:latest'); + $containerConfig->setCmd(['echo', '-n', 'output']); + $containerConfig->setAttachStdout(true); + $containerConfig->setLabels(new \ArrayObject(['docker-php-test' => 'true'])); + + $response = yield $docker->imageCreate('', [ + 'fromImage' => 'busybox:latest', + ], [], DockerAsync::FETCH_RESPONSE); + + yield $response->getBody(); + + $containerCreate = yield $docker->containerCreate($containerConfig); + $containerStart = yield $docker->containerStart($containerCreate->getId()); + $containerInfo = yield $docker->containerInspect($containerCreate->getId()); + + $this->assertSame($containerCreate->getId(), $containerInfo->getId()); + }); + } + + public function testSystemEventsAllowTheConsumptionOfDockerEvents(): void + { + $matchedEvents = []; + + Loop::run(function () use (&$matchedEvents) { + $docker = DockerAsync::create(); + + /** @var ArtaxCallbackStream $events */ + $events = yield $docker->systemEvents([ + 'filters' => \json_encode( + [ + 'type' => ['container'], + 'action' => ['create'], + ] + ), + ]); + $events->onFrame(function ($event) use (&$matchedEvents): void { + if (\is_object($event) + && $event instanceof EventsGetResponse200 + && 'create' === $event->getAction() + && 'container' === $event->getType() + ) { + $matchedEvents[] = $event; + } + }); + + $events->listen(); + + $containerConfig = new ContainersCreatePostBody(); + $containerConfig->setImage('busybox:latest'); + $containerConfig->setCmd(['echo', '-n', 'output']); + + yield $docker->containerCreate($containerConfig); + + Loop::delay(1000, function (): void { + Loop::stop(); + }); + }); + + $this->assertCount(1, $matchedEvents); + } +} diff --git a/tests/DockerClientFactoryTest.php b/tests/DockerClientFactoryTest.php new file mode 100644 index 00000000..286419d4 --- /dev/null +++ b/tests/DockerClientFactoryTest.php @@ -0,0 +1,72 @@ +assertInstanceOf(HttpClient::class, DockerClientFactory::create()); + } + + /** + * @expectedException \RuntimeException + * @expectedExceptionMessage Connection to docker has been set to use TLS, but no PATH is defined for certificate in DOCKER_CERT_PATH docker environment variable + */ + public function testCreateFromEnvWithoutCertPath(): void + { + \putenv('DOCKER_TLS_VERIFY=1'); + DockerClientFactory::createFromEnv(); + } + + public function testCreateCustomCa(): void + { + \putenv('DOCKER_TLS_VERIFY=1'); + \putenv('DOCKER_CERT_PATH=/tmp'); + + $count = \count(\get_resources('stream-context')); + $client = DockerClientFactory::createFromEnv(); + $this->assertInstanceOf(HttpClient::class, $client); + + $contexts = \get_resources('stream-context'); + $this->assertCount($count + 1, $contexts); + + // Get the last stream context. + $context = \stream_context_get_options(\end($contexts)); + $this->assertSame('/tmp/ca.pem', $context['ssl']['cafile']); + $this->assertSame('/tmp/cert.pem', $context['ssl']['local_cert']); + $this->assertSame('/tmp/key.pem', $context['ssl']['local_pk']); + } + + public function testCreateCustomPeerName(): void + { + \putenv('DOCKER_TLS_VERIFY=1'); + \putenv('DOCKER_CERT_PATH=/abc'); + \putenv('DOCKER_PEER_NAME=test'); + + $count = \count(\get_resources('stream-context')); + $client = DockerClientFactory::createFromEnv(); + $this->assertInstanceOf(HttpClient::class, $client); + + $contexts = \get_resources('stream-context'); + $this->assertCount($count + 1, $contexts); + + // Get the last stream context. + $context = \stream_context_get_options(\end($contexts)); + $this->assertSame('/abc/ca.pem', $context['ssl']['cafile']); + $this->assertSame('/abc/cert.pem', $context['ssl']['local_cert']); + $this->assertSame('/abc/key.pem', $context['ssl']['local_pk']); + $this->assertSame('test', $context['ssl']['peer_name']); + } +} diff --git a/tests/DockerTest.php b/tests/DockerTest.php new file mode 100644 index 00000000..baba2523 --- /dev/null +++ b/tests/DockerTest.php @@ -0,0 +1,15 @@ +assertInstanceOf(Docker::class, Docker::create()); + } +} diff --git a/tests/Resource/ContainerResourceTest.php b/tests/Resource/ContainerResourceTest.php new file mode 100644 index 00000000..d89f3ada --- /dev/null +++ b/tests/Resource/ContainerResourceTest.php @@ -0,0 +1,123 @@ +imageCreate('', [ + 'fromImage' => 'busybox:latest', + ]); + } + + public function testAttach(): void + { + $containerConfig = new ContainersCreatePostBody(); + $containerConfig->setImage('busybox:latest'); + $containerConfig->setCmd(['echo', '-n', 'output']); + $containerConfig->setAttachStdout(true); + $containerConfig->setLabels(new \ArrayObject(['docker-php-test' => 'true'])); + + $containerCreateResult = $this->getManager()->containerCreate($containerConfig); + $dockerRawStream = $this->getManager()->containerAttach($containerCreateResult->getId(), [ + 'stream' => true, + 'stdout' => true, + ]); + + $stdoutFull = ''; + $dockerRawStream->onStdout(function ($stdout) use (&$stdoutFull): void { + $stdoutFull .= $stdout; + }); + + $this->getManager()->containerStart($containerCreateResult->getId()); + $this->getManager()->containerWait($containerCreateResult->getId()); + + $dockerRawStream->wait(); + + $this->assertSame('output', $stdoutFull); + } + + public function testAttachWebsocket(): void + { + $containerConfig = new ContainersCreatePostBody(); + $containerConfig->setImage('busybox:latest'); + $containerConfig->setCmd(['sh']); + $containerConfig->setAttachStdout(true); + $containerConfig->setAttachStderr(true); + $containerConfig->setAttachStdin(false); + $containerConfig->setOpenStdin(true); + $containerConfig->setTty(true); + $containerConfig->setLabels(new \ArrayObject(['docker-php-test' => 'true'])); + + $containerCreateResult = $this->getManager()->containerCreate($containerConfig); + $webSocketStream = $this->getManager()->containerAttachWebsocket($containerCreateResult->getId(), [ + 'stream' => true, + 'stdout' => true, + 'stderr' => true, + 'stdin' => true, + ]); + + $this->getManager()->containerStart($containerCreateResult->getId()); + + // Read the bash first line + $webSocketStream->read(); + + // No output after that so it should be false + $this->assertFalse($webSocketStream->read()); + + // Write something to the container + $webSocketStream->write("echo test\n"); + + // Test for echo present (stdin) + $output = ''; + + while (false !== ($data = $webSocketStream->read())) { + $output .= $data; + } + + $this->assertContains('echo', $output); + + // Exit the container + $webSocketStream->write("exit\n"); + } + + public function testLogs(): void + { + $containerConfig = new ContainersCreatePostBody(); + $containerConfig->setImage('busybox:latest'); + $containerConfig->setCmd(['echo', '-n', 'output']); + $containerConfig->setAttachStdout(true); + $containerConfig->setLabels(new \ArrayObject(['docker-php-test' => 'true'])); + + $containerCreateResult = $this->getManager()->containerCreate($containerConfig); + + $this->getManager()->containerStart($containerCreateResult->getId()); + $this->getManager()->containerWait($containerCreateResult->getId()); + + $logsStream = $this->getManager()->containerLogs($containerCreateResult->getId(), [ + 'stdout' => true, + 'stderr' => true, + ], Docker::FETCH_OBJECT); + + self::assertInstanceOf(DockerRawStream::class, $logsStream); + } +} diff --git a/tests/Resource/ExecResourceTest.php b/tests/Resource/ExecResourceTest.php new file mode 100644 index 00000000..65064f2b --- /dev/null +++ b/tests/Resource/ExecResourceTest.php @@ -0,0 +1,92 @@ +createContainer(); + + $execConfig = new ContainersIdExecPostBody(); + $execConfig->setAttachStdout(true); + $execConfig->setAttachStderr(true); + $execConfig->setCmd(['echo', 'output']); + + $execCreateResult = $this->getManager()->containerExec($createContainerResult->getId(), $execConfig); + + $execStartConfig = new ExecIdStartPostBody(); + $execStartConfig->setDetach(false); + $execStartConfig->setTty(false); + + $stream = $this->getManager()->execStart($execCreateResult->getId(), $execStartConfig); + + $this->assertInstanceOf(DockerRawStream::class, $stream); + + $stdoutFull = ''; + $stream->onStdout(function ($stdout) use (&$stdoutFull): void { + $stdoutFull .= $stdout; + }); + $stream->wait(); + + $this->assertSame("output\n", $stdoutFull); + + self::getDocker()->containerKill($createContainerResult->getId(), [ + 'signal' => 'SIGKILL', + ]); + } + + public function testExecFind(): void + { + $createContainerResult = $this->createContainer(); + + $execConfig = new ContainersIdExecPostBody(); + $execConfig->setCmd(['/bin/true']); + $execCreateResult = $this->getManager()->containerExec($createContainerResult->getId(), $execConfig); + + $execStartConfig = new ExecIdStartPostBody(); + $execStartConfig->setDetach(false); + $execStartConfig->setTty(false); + + $this->getManager()->execStart($execCreateResult->getId(), $execStartConfig); + + $execFindResult = $this->getManager()->execInspect($execCreateResult->getId()); + + $this->assertInstanceOf(ExecIdJsonGetResponse200::class, $execFindResult); + + self::getDocker()->containerKill($createContainerResult->getId(), [ + 'signal' => 'SIGKILL', + ]); + } + + private function createContainer() + { + $containerConfig = new ContainersCreatePostBody(); + $containerConfig->setImage('busybox:latest'); + $containerConfig->setCmd(['sh']); + $containerConfig->setOpenStdin(true); + $containerConfig->setLabels(new \ArrayObject(['docker-php-test' => 'true'])); + + $containerCreateResult = self::getDocker()->containerCreate($containerConfig); + self::getDocker()->containerStart($containerCreateResult->getId()); + + return $containerCreateResult; + } +} diff --git a/tests/Resource/ImageResourceTest.php b/tests/Resource/ImageResourceTest.php new file mode 100644 index 00000000..e1c129dd --- /dev/null +++ b/tests/Resource/ImageResourceTest.php @@ -0,0 +1,91 @@ +from('ubuntu:precise'); + $contextBuilder->add('/test', 'test file content'); + + $context = $contextBuilder->getContext(); + $buildStream = $this->getManager()->imageBuild($context->read(), ['t' => 'test-image']); + + $this->assertInstanceOf('Docker\Stream\BuildStream', $buildStream); + + $lastMessage = ''; + + $buildStream->onFrame(function ($frame) use (&$lastMessage): void { + $lastMessage = $frame->getStream(); + }); + $buildStream->wait(); + + $this->assertContains('Successfully', $lastMessage); + } + + public function testCreate(): void + { + $createImageStream = $this->getManager()->imageCreate('', [ + 'fromImage' => 'registry:latest', + ]); + + $this->assertInstanceOf('Docker\Stream\CreateImageStream', $createImageStream); + + $firstMessage = null; + + $createImageStream->onFrame(function ($createImageInfo) use (&$firstMessage): void { + if (null === $firstMessage) { + $firstMessage = $createImageInfo->getStatus(); + } + }); + $createImageStream->wait(); + + $this->assertContains('Pulling from library/registry', $firstMessage); + } + + public function testPushStream(): void + { + $contextBuilder = new ContextBuilder(); + $contextBuilder->from('ubuntu:precise'); + $contextBuilder->add('/test', 'test file content'); + + $context = $contextBuilder->getContext(); + $this->getManager()->imageBuild($context->read(), ['t' => 'localhost:5000/test-image'], [], Client::FETCH_OBJECT); + + $registryConfig = new AuthConfig(); + $registryConfig->setServeraddress('localhost:5000'); + $pushImageStream = $this->getManager()->imagePush('localhost:5000/test-image', [], [ + 'X-Registry-Auth' => $registryConfig, + ]); + + $this->assertInstanceOf('Docker\Stream\PushStream', $pushImageStream); + + $firstMessage = null; + + $pushImageStream->onFrame(function ($pushImageInfo) use (&$firstMessage): void { + if (null === $firstMessage) { + $firstMessage = $pushImageInfo->getStatus(); + } + }); + $pushImageStream->wait(); + + $this->assertContains('repository [localhost:5000/test-image]', $firstMessage); + } +} diff --git a/tests/Resource/SystemResourceTest.php b/tests/Resource/SystemResourceTest.php new file mode 100644 index 00000000..474f3506 --- /dev/null +++ b/tests/Resource/SystemResourceTest.php @@ -0,0 +1,41 @@ +getManager()->systemEvents([ + 'since' => (string) (\time() - 1), + 'until' => (string) (\time() + 4), + ]); + + $lastEvent = null; + + $stream->onFrame(function ($event) use (&$lastEvent): void { + $lastEvent = $event; + }); + + self::getDocker()->imageCreate('', [ + 'fromImage' => 'busybox:latest', + ]); + + $stream->wait(); + + $this->assertInstanceOf(EventsGetResponse200::class, $lastEvent); + } +} diff --git a/src/Docker/Tests/Manager/script/daemon.sh b/tests/Resource/script/daemon.sh similarity index 100% rename from src/Docker/Tests/Manager/script/daemon.sh rename to tests/Resource/script/daemon.sh diff --git a/src/Docker/Tests/Manager/script/kill.sh b/tests/Resource/script/kill.sh similarity index 100% rename from src/Docker/Tests/Manager/script/kill.sh rename to tests/Resource/script/kill.sh diff --git a/tests/Stream/MultiJsonStreamTest.php b/tests/Stream/MultiJsonStreamTest.php new file mode 100644 index 00000000..a999f492 --- /dev/null +++ b/tests/Stream/MultiJsonStreamTest.php @@ -0,0 +1,61 @@ +write($jsonStream); + + $serializer = $this->getMockBuilder(SerializerInterface::class) + ->getMock(); + + $serializer + ->expects($this->exactly(\count($jsonParts))) + ->method('deserialize') + ->withConsecutive(...\array_map(function ($part) { + return [$part, BuildInfo::class, 'json', []]; + }, $jsonParts)) + ; + + $stub = $this->getMockForAbstractClass(MultiJsonStream::class, [$stream, $serializer]); + $stub->expects($this->any()) + ->method('getDecodeClass') + ->willReturn('BuildInfo'); + + $stub->wait(); + } +} diff --git a/tests/TestCase.php b/tests/TestCase.php new file mode 100644 index 00000000..46097c0f --- /dev/null +++ b/tests/TestCase.php @@ -0,0 +1,21 @@ +