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 f0a81ed2..41708d6f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,24 +1,68 @@ language: php +sudo: required + +services: + - docker + +cache: + directories: + - $HOME/.composer/cache + +php: 7.2 + +env: + global: + - TEST_COMMAND="composer test" + matrix: + - 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 -php: - - 5.4 - - 5.5 - - 5.6 - - hhvm - matrix: - allow_failures: - - php: hhvm + 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_script: - - sudo sh -c "wget -qO- https://get.docker.io/gpg | apt-key add -" - - sudo sh -c "echo deb http://get.docker.io/ubuntu docker main > /etc/apt/sources.list.d/docker.list" +before_install: - sudo apt-get update - - echo exit 101 | sudo tee /usr/sbin/policy-rc.d - - sudo chmod +x /usr/sbin/policy-rc.d - - sudo apt-get install -qy slirp lxc lxc-docker - - git clone git://github.com/jpetazzo/sekexe - - COMPOSER_ROOT_VERSION=dev-master composer --prefer-source --dev install + - 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: + - 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: - - sekexe/run "`pwd`/docker.sh" + - $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 c17abcff..27464be6 100644 --- a/README.md +++ b/README.md @@ -1,49 +1,52 @@ -Docker PHP -========== - -**Docker PHP** (for lack of a better name) is a [Docker](http://docker.io/) 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.9](http://docs.docker.io/en/latest/api/docker_remote_api_v1.9/). - -[![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 ----- -See [the documentation](https://github.com/stage1/docker-php/blob/master/doc/usage.md). +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 e8f60a21..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.5.0", - "symfony/process": "~2.5.0", - "guzzlehttp/guzzle": "~4.1.0", - "guzzlehttp/streams": "~1.3.0" + "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/doc/usage.md b/doc/usage.md deleted file mode 100644 index 5851752e..00000000 --- a/doc/usage.md +++ /dev/null @@ -1,210 +0,0 @@ -# Docker-PHP - -* [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) - -## Connecting to Docker - -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`: - -```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 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); -``` diff --git a/docker.sh b/docker.sh deleted file mode 100755 index eedfabde..00000000 --- a/docker.sh +++ /dev/null @@ -1,25 +0,0 @@ -#!/bin/bash - -export PATH=$PATH:/home/travis/.phpenv/bin -eval "$(phpenv init -)" -phpenv rehash 2>/dev/null - -# Exit on first error -set -e - -# Mount rount -mount -t tmpfs none /root - -# Kill background processes on exit -trap 'kill $(jobs -p)' SIGINT SIGTERM EXIT - -# Workaround with cgroup for recent version of docker -cgroups-umount -cgroups-mount - -# Start docker daemon -docker -d -H 0.0.0.0:4243 -H unix:///var/run/docker.sock 2>> /dev/null >> /dev/null & -sleep 2 - -# $init is set by sekexe -cd $(dirname $init)/.. && php bin/phpunit -c phpunit.xml.dist \ No newline at end of file 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 new file mode 100644 index 00000000..52a7f200 --- /dev/null +++ b/docs/basic.md @@ -0,0 +1,19 @@ +# Basic Usage + +`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. + +As an example for listing container you can do: + +```php +containerList(); + +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 +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/index.md b/docs/index.md new file mode 100644 index 00000000..d00780dc --- /dev/null +++ b/docs/index.md @@ -0,0 +1,22 @@ +# Docker-PHP + +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 new file mode 100644 index 00000000..2cc4fcec --- /dev/null +++ b/docs/installation.md @@ -0,0 +1,21 @@ +# Installation + +The recommended way to install Docker PHP is to use [Composer](http://getcomposer.org/): + +Run `composer require docker-php/docker-php` to add the dependency + +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`. + +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 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 new file mode 100644 index 00000000..1f127ad3 --- /dev/null +++ b/mkdocs.yml @@ -0,0 +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/docker-php/docker-php + +pages: + - 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->fs = $fs ?: new Filesystem(); $this->format = $format; + $this->fs = $fs ?? new Filesystem(); } /** - * Get directory of Context + * Get directory of Context. * * @return string */ @@ -63,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; } /** @@ -99,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 * @@ -118,15 +125,15 @@ public function toTar() } /** - * Return a stream for this context + * Return a stream for this context. * - * @return stream 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 .", array(array("pipe", "r"), array("pipe", "w"), array("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; @@ -134,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 79% rename from src/Docker/Context/ContextInterface.php rename to src/Context/ContextInterface.php index 2da05a1e..0e1fc283 100644 --- a/src/Docker/Context/ContextInterface.php +++ b/src/Context/ContextInterface.php @@ -1,23 +1,25 @@ 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 9efb3f73..00000000 --- a/src/Docker/Container.php +++ /dev/null @@ -1,376 +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)) { - 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 Docker\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|Docker\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) { - $this->image = new Image(); - $this->image->setName($this->config['Image']); - } - - 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; - } -} \ No newline at end of file diff --git a/src/Docker/Context/ContextBuilder.php b/src/Docker/Context/ContextBuilder.php deleted file mode 100644 index 23d8742e..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); - } -} \ No newline at end of file diff --git a/src/Docker/Docker.php b/src/Docker/Docker.php deleted file mode 100644 index 442d3db6..00000000 --- a/src/Docker/Docker.php +++ /dev/null @@ -1,157 +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; - } - - /** - * 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 - */ - public function build(ContextInterface $context, $name, callable $callback = null, $quiet = false, $cache = true, $rm = false) - { - $content = is_resource($context->read()) ? new Stream($context->read()) : $context->read(); - $response = $this->httpClient->post(['/build{?data*}', ['data' => [ - 'q' => (integer) $quiet, - 't' => $name, - 'nocache' => (integer) !$cache, - 'rm' => (integer) $rm - ]]], [ - 'headers' => array('Content-Type' => 'application/tar'), - 'body' => $content, - 'stream' => true - ]); - - if (null === $callback) { - $callback = function() {}; - } - - $stream = $response->getBody(); - - if ($stream instanceof StreamCallbackInterface) { - $stream->readWithCallback($callback); - } else { - $callback($stream->__toString(), null); - } - } - - /** - * Commit a container into an image - * - * @param \Docker\Container $container - * @param array $config - * - * @throws Exception\UnexpectedStatusCodeException - * - * @return \Docker\Image - * - * @see http://docs.docker.io/en/latest/api/docker_remote_api_v1.7/#create-a-new-image-from-a-container-s-changes - */ - public function commit(Container $container, $config = array()) - { - 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 adc5bd20..00000000 --- a/src/Docker/Exception.php +++ /dev/null @@ -1,13 +0,0 @@ -getStatusCode(), trim((string) $response->getBody())); - } -} \ No newline at end of file diff --git a/src/Docker/HostConfig.php b/src/Docker/HostConfig.php deleted file mode 100644 index b0545495..00000000 --- a/src/Docker/HostConfig.php +++ /dev/null @@ -1,26 +0,0 @@ -env = $env; - - return $this; - } -} \ No newline at end of file diff --git a/src/Docker/Http/Adapter/DockerAdapter.php b/src/Docker/Http/Adapter/DockerAdapter.php deleted file mode 100644 index ab975b7e..00000000 --- a/src/Docker/Http/Adapter/DockerAdapter.php +++ /dev/null @@ -1,201 +0,0 @@ -entrypoint = $entrypoint; - $this->messageFactory = $messageFactory; - } - - /** - * 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'); - } - - RequestEvents::emitBefore($transaction); - if (!$transaction->getResponse()) { - $this->createResponse($transaction); - RequestEvents::emitComplete($transaction); - } - - return $transaction->getResponse(); - } - - private function createResponse(TransactionInterface $transaction) - { - $errorNo = null; - $errorMsg = null; - - $request = $transaction->getRequest(); - - if ($request->getConfig()['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)); - if(!$socket) { - throw new RequestException(sprintf('Cannot open socket connection: %s [code %d]', $errorMsg, $errorNo), $request); - } - - // Write headers - fwrite($socket, $this->getRequestHeaderAsString($request)); - - // Write body if set - if ($request->getBody() !== null) { - if ($request->getConfig()['stream']) { - $stream = $request->getBody(); - - while (!$stream->eof()) { - $read = $stream->read(self::CHUNK_SIZE); - $frame = dechex(mb_strlen($read))."\r\n".$read."\r\n"; - - fwrite($socket, $frame); - } - - $stream->close(); - - fwrite($socket, "0\r\n\r\n"); - } else { - fwrite($socket, $request->getBody()->__toString()); - } - } - - stream_set_timeout($socket, $this->getDefaultTimeout($transaction)); - - // Response should be available, extract headers - do { - $response = $this->getResponseWithHeaders($socket); - } while($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); - } - - $this->setResponseStream($response, $socket); - $transaction->setResponse($response); - - return $response; - } - - private function getResponseWithHeaders($stream) - { - $headers = array(); - - while (($line = fgets($stream)) !== false) { - - if (rtrim($line) === '') { - break; - } - - $headers[] = trim($line); - } - - $parts = explode(' ', array_shift($headers), 3); - $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) - { - if ($response->getHeader('Transfer-Encoding') == "chunked") { - $stream = new ChunkedStream($socket); - } elseif ($response->getHeader('Content-Type') == "application/vnd.docker.raw-stream") { - $stream = new AttachStream($socket); - } else { - $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; - } -} diff --git a/src/Docker/Http/DockerClient.php b/src/Docker/Http/DockerClient.php deleted file mode 100644 index d0c74c5c..00000000 --- a/src/Docker/Http/DockerClient.php +++ /dev/null @@ -1,30 +0,0 @@ -eof()) { - list($content, $type) = $this->readPart(); - - if ($content !== null) { - $callback($content, $type); - } - } - } - - protected function readPart() - { - $header = $this->read(8); - - if (strlen($header) < 8) { - return array(null, null); - } - - $decoded = unpack('C1stream_type/C3/N1size', $header); - - return array($this->read($decoded['size']), $decoded['stream_type']); - } -} diff --git a/src/Docker/Http/Stream/ChunkedStream.php b/src/Docker/Http/Stream/ChunkedStream.php deleted file mode 100644 index 5b245964..00000000 --- a/src/Docker/Http/Stream/ChunkedStream.php +++ /dev/null @@ -1,109 +0,0 @@ -eof()) { - $content = $this->readPart(); - - if ($content == null) { - break; - } - - $callback($content, null); - } - } - - - /** - * Cast stream to string - * - * @return string - */ - public function __toString() - { - return (string) $this->getContents(); - } - - - /** - * Gets the string contents of the stream - * - * @param integer $maxlength Returns contents truncated to supplied length (-1 for max) - * - * @return string - */ - public function getContents($maxlength = -1) - { - $contents = ''; - - while (!$this->eof()) { - $part = $this->readPart(); - - if ($part == null) { - break; - } - - $contents .= $part; - - if ($maxlength > -1 && strlen($contents) > $maxlength) { - return substr($contents, 0, $maxlength); - } - } - - return $contents; - } - - - /** - * Read a chunk from the stream - * - * @return string - */ - protected function readPart() - { - $tmpSize = ""; - - while(($read = (string)$this->read(1)) != "\n") { - $tmpSize .= $read; - - if ($this->eof()) { - return null; - } - } - - $size = hexdec(trim($tmpSize)); - - if ($size > 0) { - $part = $this->read($size); - } else { - $part = ""; - } - - while($this->read(1) != "\n") { - if ($this->eof()) { - return null; - } - } - - return $part; - } -} diff --git a/src/Docker/Http/Stream/StreamCallbackInterface.php b/src/Docker/Http/Stream/StreamCallbackInterface.php deleted file mode 100644 index 53882ba6..00000000 --- a/src/Docker/Http/Stream/StreamCallbackInterface.php +++ /dev/null @@ -1,24 +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; - } -} \ No newline at end of file diff --git a/src/Docker/Json.php b/src/Docker/Json.php deleted file mode 100644 index 08beb3ca..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 = array()) - { - $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 \Docker\Exception\ContainerNotFoundException - * - * @return ContainerManager - */ - public function inspect(Container $container) - { - try { - $response = $this->client->get(['/containers/{id}/json', ['id' => $container->getId()]]); - } catch (ClientException $e) { - if ($e->getResponse()->getStatusCode() == "404") { - throw new ContainerNotFoundException($container->getId(), $e); - } - - throw $e; - } - - $container->setRuntimeInformations($response->json()); - - return $this; - } - - /** - * 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(), - ], - ]], - array( - 'body' => Json::encode($container->getConfig()), - 'headers' => array('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 = array()) - { - $response = $this->client->post(['/containers/{id}/start', ['id' => $container->getId()]], array( - 'body' => Json::encode($hostConfig), - 'headers' => array('content-type' => 'application/json') - )); - - 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 = array(), $daemon = false, $timeout = null) - { - $this->create($container); - - if (null !== $attachCallback) { - $attachResponse = $this->attach($container, true, true, true, true, true, $timeout); - } - - $this->start($container, $hostConfig); - - if (null !== $attachCallback && $attachResponse) { - $attachResponse->getBody()->readWithCallback($attachCallback); - } - - if (!$daemon) { - $this->wait($container); - - return ($container->getExitCode() == 0); - } - - return null; - } - - /** - * @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 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 \Docker\Http\Response Re - */ - public function attach(Container $container, $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 - ] - ]], array( - 'timeout' => $timeout !== null ? $timeout : $this->client->getDefaultOption('timeout') - )); - - if ($response->getStatusCode() !== "200") { - throw UnexpectedStatusCodeException::fromResponse($response); - } - - return $response; - } - - /** - * Wait for a container to finish - * - * @param \Docker\Container $container - * - * @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()]], array( - '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 - ]]); - - if ($response->getStatusCode() !== "204") { - throw UnexpectedStatusCodeException::fromResponse($response); - } - - $this->inspect($container); - - return $this; - } - - /** - * Delete a container from docker server - * - * @param \Docker\Container $container - * @param boolean $volumes - * - * @return \Docker\Manager\ContainerManager - */ - public function remove(Container $container, $volumes = false) - { - $response = $this->client->delete(['/containers/{id}?v={volumes}', [ - 'id' => $container->getId(), - 'v' => $volumes - ]]); - - if ($response->getStatusCode() !== "204") { - throw UnexpectedStatusCodeException::fromResponse($response); - } - - return $this; - } -} \ No newline at end of file diff --git a/src/Docker/Manager/ImageManager.php b/src/Docker/Manager/ImageManager.php deleted file mode 100644 index 1782f6f4..00000000 --- a/src/Docker/Manager/ImageManager.php +++ /dev/null @@ -1,191 +0,0 @@ -client = $client; - } - - /** - * Get all images in docker daemon - * - * @throws \Docker\Exception\UnexpectedStatusCodeException - * - * @return Image[] - */ - public function findAll() - { - $response = $this->client->get('/images/json'); - - if ($response->getStatusCode() !== "200") { - throw UnexpectedStatusCodeException::fromResponse($response); - } - - $coll = []; - $stream = $response->getBody(); - $imageString = ""; - - if ($stream instanceof StreamCallbackInterface) { - $stream->readWithCallback(function ($output) use(&$imageString) { - $imageString .= $output; - }); - } else { - $imageString = $response->getBody()->__toString(); - } - - $images = json_decode($imageString, true); - - if (!is_array($images)) { - return []; - } - - 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 to latest) - * - * @return Image - */ - public function find($repository, $tag = 'latest') - { - $image = new Image($repository, $tag); - - $this->inspect($image); - - return $image; - } - - /** - * Inspect a image - * - * @param \Docker\Image $image - * - * @throws \Docker\Exception\ImageNotFoundException - * @throws \Docker\Exception\UnexpectedStatusCodeException - * @throws \GuzzleHttp\Exception\ClientException - * - * @return ImageManager - */ - public function inspect(Image $image) - { - try { - $response = $this->client->get(['/images/{id}/json', ['id' => $image->__toString()]]); - } catch (ClientException $e) { - if ($e->getResponse()->getStatusCode() == "404") { - throw new ImageNotFoundException($image->__toString(), $e); - } - - throw $e; - } - - if ($response->getStatusCode() !== "200") { - throw UnexpectedStatusCodeException::fromResponse($response); - } - - $data = $response->json(); - - $image->setId($data['Id']); - // @TODO Add extra info on image - - return $this; - } - - /** - * Pull an image from registry - * - * @param string $name Name of image to pull - * @param string $tag Tag of image - * - * @throws \Docker\Exception\UnexpectedStatusCodeException - * - * @return Image - */ - public function pull($name, $tag = 'latest') - { - $response = $this->client->post(['/images/create?fromImage={image}&tag={tag}', ['image' => $name, 'tag' => $tag]]); - - if ($response->getStatusCode() !== "200") { - throw UnexpectedStatusCodeException::fromResponse($response); - } - - $stream = $response->getBody(); - - if ($stream instanceof StreamCallbackInterface) { - $stream->readWithCallback(function () {}); - } else { - $stream->__toString(); - } - - $image = new Image($name, $tag); - - $this->inspect($image); - - return $image; - } - - /** - * Delete an image from docker daemon - * - * @param Image $image Image to delete - * @param boolean $force Force deletion of image (default false) - * @param boolean $noprune Do not delete parent images (default false) - * - * @throws \Docker\Exception\UnexpectedStatusCodeException - * - * @return ImageManager - */ - public function delete(Image $image, $force = false, $noprune = false) - { - $response = $this->client->delete(['/images/{image}?force={force}&noprune={noprune}', [ - 'image' => $image->__toString(), - 'force' => $force, - 'noprune' => $noprune - ]]); - - if ($response->getStatusCode() !== "200") { - throw UnexpectedStatusCodeException::fromResponse($response); - } - - return $this; - } -} \ No newline at end of file diff --git a/src/Docker/Port.php b/src/Docker/Port.php deleted file mode 100644 index 293949d7..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 - * - * @return array - */ - static public 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; - } -} \ No newline at end of file diff --git a/src/Docker/PortCollection.php b/src/Docker/PortCollection.php deleted file mode 100644 index b7be7773..00000000 --- a/src/Docker/PortCollection.php +++ /dev/null @@ -1,82 +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; - } - - /** - * @param Docker\Port - * - * @return PortCollection - */ - public function add(Port $port) - { - $this->ports[] = $port; - - return $this; - } - - /** - * @return array - */ - public function all() - { - return $this->ports; - } -} \ No newline at end of file diff --git a/src/Docker/PortSpecInterface.php b/src/Docker/PortSpecInterface.php deleted file mode 100644 index b18b969d..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!'); - } - -} \ No newline at end of file diff --git a/src/Docker/Tests/Context/ContextBuilderTest.php b/src/Docker/Tests/Context/ContextBuilderTest.php deleted file mode 100644 index 6bc0d028..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', <<markTestSkipped('No /bin/tar on host'); - } - - $directory = __DIR__.DIRECTORY_SEPARATOR."context-test"; - - $context = new Context($directory); - $process = new Process('/bin/tar c .', $directory); - $process->run(); - - $this->assertEquals(md5($process->getOutput()), md5($context->toTar())); - } - - public function testReturnsValidTarStream() - { - if (!file_exists('/bin/tar')) { - $this->markTestSkipped('No /bin/tar on host'); - } - - $directory = __DIR__.DIRECTORY_SEPARATOR."context-test"; - - $context = new Context($directory); - $this->assertInternalType('resource', $context->toStream()); - } -} \ No newline at end of file diff --git a/src/Docker/Tests/DockerTest.php b/src/Docker/Tests/DockerTest.php deleted file mode 100644 index eed9abc6..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(); - - $docker->build($contextBuilder->getContext(), 'foo', function($output) use(&$content) { - $content .= $output; - }); - - $this->assertRegExp('/Successfully built/', $content); - } - - public function testBuildWithExistingDirectory() - { - if (!file_exists('/bin/tar')) { - $this->markTestSkipped('No /bin/tar on host'); - } - - $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) { - $content .= $output; - $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()); - } -} \ No newline at end of file diff --git a/src/Docker/Tests/Manager/ContainerManagerTest.php b/src/Docker/Tests/Manager/ContainerManagerTest.php deleted file mode 100644 index 435d7845..00000000 --- a/src/Docker/Tests/Manager/ContainerManagerTest.php +++ /dev/null @@ -1,313 +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 testCreateThrowsRightFormedException() - { - $container = new Container(['Image' => 'non-existent']); - - $manager = $this->getManager(); - - try { - $manager->create($container); - } catch (\GuzzleHttp\Exception\ClientException $e) { - $this->assertEquals("404", $e->getResponse()->getStatusCode()); - $this->assertContains('No such image: non-existent (tag: latest)', $e->getResponse()->getBody()->__toString()); - } - } - - 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(array('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(array('create', 'start', 'wait', 'attach')) - ->disableOriginalConstructor() - ->getMock(); - - $response = $this->getMockBuilder('\GuzzleHttp\Message\Response')->disableOriginalConstructor()->getMock(); - $stream = $this->getMockBuilder('\Docker\Http\Stream\AttachStream')->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(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)); - - $stream->expects($this->once()) - ->method('readWithCallback') - ->with($callback); - - $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(array('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, array(), 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); - $manager->start($container); - - $response->getBody()->readWithCallback(function ($log, $stdtype) use(&$type, &$output) { - $type = $stdtype; - $output = $log; - }); - - $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); - $manager->start($container); - - $response->getBody()->readWithCallback(function ($log, $stdtype) use(&$type, &$output) { - $type = $stdtype; - $output = $log; - }); - - $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() - { - $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() - { - $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); - } - -} \ No newline at end of file diff --git a/src/Docker/Tests/Manager/ImageManagerTest.php b/src/Docker/Tests/Manager/ImageManagerTest.php deleted file mode 100644 index 08704b70..00000000 --- a/src/Docker/Tests/Manager/ImageManagerTest.php +++ /dev/null @@ -1,65 +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', 'precise'); - - $this->assertEquals('ubuntu', $image->getRepository()); - $this->assertEquals('precise', $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 testDelete() - { - $manager = $this->getManager(); - - $image = $manager->find('ubuntu', 'precise'); - $manager->delete($image); - - $this->setExpectedException('\\Docker\\Exception\\ImageNotFoundException', 'Image not found'); - $manager->inspect($image); - } -} \ No newline at end of file diff --git a/src/Docker/Tests/PortCollectionTest.php b/src/Docker/Tests/PortCollectionTest.php deleted file mode 100644 index 3df05d12..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()); - } -} \ No newline at end of file diff --git a/src/Docker/Tests/PortTest.php b/src/Docker/Tests/PortTest.php deleted file mode 100644 index 86502f75..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' - ], - ]; - } -} \ No newline at end of file diff --git a/src/Docker/Tests/TestCase.php b/src/Docker/Tests/TestCase.php deleted file mode 100644 index 103d4b3f..00000000 --- a/src/Docker/Tests/TestCase.php +++ /dev/null @@ -1,23 +0,0 @@ -docker) { - $this->docker = new Docker($client); - } - - return $this->docker; - } -} \ No newline at end of file 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 63% rename from src/Docker/Tests/Context/context-test/Dockerfile rename to tests/Context/context-test/Dockerfile index 7d863064..5cf0c8ad 100644 --- a/src/Docker/Tests/Context/context-test/Dockerfile +++ b/tests/Context/context-test/Dockerfile @@ -1,3 +1,3 @@ -FROM base +FROM busybox RUN echo 'hello world' \ No newline at end of file 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/tests/Resource/script/daemon.sh b/tests/Resource/script/daemon.sh new file mode 100755 index 00000000..8231b1de --- /dev/null +++ b/tests/Resource/script/daemon.sh @@ -0,0 +1,12 @@ +#!/bin/bash + +terminate() { + echo "test" + exit 0 +} + +trap terminate SIGHUP SIGINT SIGTERM + +while true; do + sleep 1000 & wait +done diff --git a/tests/Resource/script/kill.sh b/tests/Resource/script/kill.sh new file mode 100644 index 00000000..0b17b0bc --- /dev/null +++ b/tests/Resource/script/kill.sh @@ -0,0 +1,20 @@ +#!/bin/bash + +trap_with_arg() { + func="$1" ; shift + for sig ; do + trap "$func $sig" "$sig" + done +} + +func_trap() { + echo $1 + exit 0 +} + +trap_with_arg func_trap HUP INT TERM EXIT + +while true; do + sleep 1000 & wait +done + 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 @@ +