GeezerVison is showing github.com — feat!: support Docker Engine API v1.45 to v1.56 · docker-php/docker-php@f2148a9
File treeExpand file treeCollapse file tree Open diff view settings Expand file treeCollapse file tree Open diff view settings Collapse file Copy file name to clipboardExpand all lines: .github/workflows/continuous-integration.yml +59Lines changed: 59 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
| 53 | 53 |
| 54 | 54 | - name: Run PHPUnit test suite | 55 | 55 | run: composer run-script test-ci | | 56 | + | | 57 | + api-versions: | | 58 | + name: API ${{ matrix.api-line }} (Docker ${{ matrix.docker }}) | | 59 | + | | 60 | + runs-on: ubuntu-latest | | 61 | + | | 62 | + strategy: | | 63 | + fail-fast: false | | 64 | + matrix: | | 65 | + api-line: ["7.1.45", "7.1.46", "7.1.47", "7.1.48", "7.1.49", "7.1.50", "7.1.51", "7.1.52", "7.1.53", "7.1.54", "7.1.55", "7.1.56"] | | 66 | + # The runner's daemon is older than the newest lines, so those negotiate down. | | 67 | + docker: [runner] | | 68 | + include: | | 69 | + # Run the newest line against a daemon that supports it without negotiation. | | 70 | + - api-line: "7.1.56" | | 71 | + docker: "29" | | 72 | + | | 73 | + services: | | 74 | + dind: | | 75 | + image: ${{ matrix.docker == '29' && 'docker:29-dind' || '' }} | | 76 | + options: --privileged | | 77 | + env: | | 78 | + DOCKER_TLS_CERTDIR: "" | | 79 | + ports: | | 80 | + - 2375:2375 | | 81 | + | | 82 | + env: | | 83 | + DOCKER_HOST: ${{ matrix.docker == '29' && 'tcp://127.0.0.1:2375' || '' }} | | 84 | + | | 85 | + steps: | | 86 | + - name: Repository checkout | | 87 | + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | | 88 | + | | 89 | + - name: Setup PHP with PECL extension | | 90 | + uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # v2.37.2 | | 91 | + with: | | 92 | + php-version: "8.5" | | 93 | + extensions: curl, openssl | | 94 | + tools: composer:v2 | | 95 | + coverage: none | | 96 | + | | 97 | + - name: Select the API line | | 98 | + run: composer require --no-update "docker-php/docker-php-api:>=${{ matrix.api-line }}.0 <${{ matrix.api-line }}.99999" | | 99 | + | | 100 | + - name: Install dependencies | | 101 | + uses: ramsey/composer-install@65e4f84970763564f46a70b8a54b90d033b3bdda # v4.0.0 | | 102 | + with: | | 103 | + dependency-versions: highest | | 104 | + | | 105 | + - name: Wait for the daemon | | 106 | + run: | | | 107 | + for attempt in $(seq 1 30); do docker version && break; sleep 2; done | | 108 | + docker version --format 'Daemon API {{.Server.APIVersion}}' | | 109 | +
| | 110 | + - name: Pull the docker image used by the tests. | | 111 | + run: docker pull busybox:latest | | 112 | + | | 113 | + - name: Run PHPUnit test suite | | 114 | + run: composer run-script test-ci |
Collapse file Copy file name to clipboardExpand all lines: composer.json +1-2Lines changed: 1 addition & 2 deletions | Original file line number | Diff line number | Diff line change |
|---|
| 11 | 11 | ], | 12 | 12 | "require": { | 13 | 13 | "php": ">=8.1", | 14 | | - "docker-php/docker-php-api": ">=7.1.45.0 <7.1.46.0", | | 14 | + "docker-php/docker-php-api": ">=7.1.45.0 <7.1.57.0", | 15 | 15 | "nyholm/psr7": "^1.8", | 16 | 16 | "php-http/client-common": "^2.7", | 17 | 17 | "php-http/discovery": "^1.19", | 18 | 18 | "php-http/socket-client": "^2.3", | 19 | 19 | "psr/http-message": "^2.0", | 20 | | - "symfony/deprecation-contracts": "^2.5 || ^3.0", | 21 | 20 | "symfony/filesystem": "^6.3 || ^7.0 || ^8.0", | 22 | 21 | "symfony/process": "^6.3 || ^7.0 || ^8.0", | 23 | 22 | "symfony/serializer": "^6.3 || ^7.0 || ^8.0" | |
Collapse file Copy file name to clipboardExpand all lines: src/Context/Context.php +5-16Lines changed: 5 additions & 16 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -47,7 +47,7 @@ class Context implements ContextInterface | 47 | 47 | */ | 48 | 48 | private $format = self::FORMAT_STREAM; | 49 | 49 |
| 50 | | - private bool $applyDockerignore = false; | | 50 | + private bool $applyDockerignore = true; | 51 | 51 |
| 52 | 52 | /** Temporary NUL-separated list of the paths to archive. */ | 53 | 53 | private ?string $pathList = null; | @@ -95,10 +95,10 @@ public function getDockerfileContent() | 95 | 95 | } | 96 | 96 |
| 97 | 97 | /** | 98 | | - * Leave out paths matched by the context's .dockerignore file, as `docker build` does. | | 98 | + * Whether to leave out paths matched by the context's .dockerignore file, | | 99 | + * as `docker build` does (the default). | 99 | 100 | * | 100 | | - * Without this, the whole directory is archived and a .dockerignore file | 101 | | - * triggers a deprecation notice. Applying it becomes the default in 4.0. | | 101 | + * Pass false to archive the whole directory, as 3.x did by default. | 102 | 102 | */ | 103 | 103 | public function applyDockerignore(bool $enabled = true): static | 104 | 104 | { | @@ -182,18 +182,7 @@ public function __destruct() | 182 | 182 | private function tarCommand(): array | 183 | 183 | { | 184 | 184 | $dockerignore = $this->directory.\DIRECTORY_SEPARATOR.'.dockerignore'; | 185 | | - if (!is_file($dockerignore)) { | 186 | | - return ['/usr/bin/env', 'tar', '-c', '.']; | 187 | | - } | 188 | | - | 189 | | - if (!$this->applyDockerignore) { | 190 | | - trigger_deprecation( | 191 | | - 'docker-php/docker-php', | 192 | | - '3.3', | 193 | | - 'The build context %s has a .dockerignore file that is not applied; from 4.0 it will be. Call Context::applyDockerignore() to opt in now.', | 194 | | - $this->directory | 195 | | - ); | 196 | | - | | 185 | + if (!$this->applyDockerignore || !is_file($dockerignore)) { | 197 | 186 | return ['/usr/bin/env', 'tar', '-c', '.']; | 198 | 187 | } | 199 | 188 |
| |
Collapse file Copy file name to clipboardExpand all lines: src/Docker.php +23-20Lines changed: 23 additions & 20 deletions | Original file line number | Diff line number | Diff line change |
|---|
| 18 | 18 | use Docker\Endpoint\ImagePush; | 19 | 19 | use Docker\Endpoint\InteractiveExecStart; | 20 | 20 | use Docker\Endpoint\SystemEvents; | | 21 | +use Docker\API\Normalizer\JaneObjectNormalizer; | 21 | 22 | use Docker\API\Runtime\Client\Endpoint; | | 23 | +use Docker\API\Runtime\Client\FormEncoder; | 22 | 24 | use Docker\Exception\BadRequestException; | 23 | 25 | use Docker\Exception\UnexpectedStatusCodeException; | 24 | 26 | use Docker\Http\InteractiveHttpClient; | | 27 | +use Docker\Serializer\LargeIntegerJsonDecode; | 25 | 28 | use Docker\Stream\AttachWebsocketStream; | 26 | 29 | use Docker\Stream\BuildStream; | 27 | 30 | use Docker\Stream\CreateImageStream; | | 34 | 37 | use Http\Client\Socket\Stream as SocketStream; | 35 | 38 | use Nyholm\Psr7\Stream; | 36 | 39 | use Psr\Http\Message\ResponseInterface; | | 40 | +use Symfony\Component\Serializer\Encoder\JsonEncode; | | 41 | +use Symfony\Component\Serializer\Encoder\JsonEncoder; | | 42 | +use Symfony\Component\Serializer\Normalizer\ArrayDenormalizer; | | 43 | +use Symfony\Component\Serializer\Serializer; | 37 | 44 |
| 38 | 45 | /** | 39 | 46 | * Docker\Docker. | @@ -42,14 +49,13 @@ class Docker extends Client | 42 | 49 | { | 43 | 50 | private bool $interactiveExecEnabled = false; | 44 | 51 |
| 45 | | - private bool $throwOnUnexpectedStatus = false; | | 52 | + private bool $throwOnUnexpectedStatus = true; | 46 | 53 |
| 47 | 54 | /** | 48 | | - * Throw an UnexpectedStatusCodeException when Docker returns an error | 49 | | - * status that the generated endpoint does not handle. | | 55 | + * Whether an error status that the generated endpoint does not handle | | 56 | + * throws an UnexpectedStatusCodeException (the default). | 50 | 57 | * | 51 | | - * Without this, such calls return null as in earlier releases and trigger | 52 | | - * a deprecation notice. Throwing becomes the default in 4.0. | | 58 | + * Pass false to return null for those responses, as 3.x did by default. | 53 | 59 | */ | 54 | 60 | public function throwOnUnexpectedStatus(bool $enabled = true): static | 55 | 61 | { | @@ -77,19 +83,10 @@ public function executeEndpoint(Endpoint $endpoint, string $fetch = self::FETCH_ | 77 | 83 | return $result; | 78 | 84 | } | 79 | 85 |
| 80 | | - $exception = UnexpectedStatusCodeException::fromResponse($endpoint->getMethod(), $endpoint->getUri(), $response); | 81 | 86 | if ($this->throwOnUnexpectedStatus) { | 82 | | - throw $exception; | | 87 | + throw UnexpectedStatusCodeException::fromResponse($endpoint->getMethod(), $endpoint->getUri(), $response); | 83 | 88 | } | 84 | 89 |
| 85 | | - trigger_deprecation( | 86 | | - 'docker-php/docker-php', | 87 | | - '3.3', | 88 | | - '%s The call returned null; from 4.0 it will throw %s. Call Docker::throwOnUnexpectedStatus() to opt in now.', | 89 | | - $exception->getMessage(), | 90 | | - $exception::class | 91 | | - ); | 92 | | - | 93 | 90 | return null; | 94 | 91 | } | 95 | 92 |
| @@ -137,10 +134,11 @@ public function containerLogs(string $id, array $queryParameters = [], string $f | 137 | 134 | * {@inheritdoc} | 138 | 135 | * | 139 | 136 | * With `stream => true` (the default) the result is a StatsStream that | 140 | | - * delivers one sample per frame; with `stream => false` it is one decoded | 141 | | - * sample. | | 137 | + * delivers one sample per frame; with `stream => false` it is one sample. | | 138 | + * Samples are ContainerStatsResponse models from API 1.48, and decoded | | 139 | + * \stdClass objects for earlier API versions. | 142 | 140 | * | 143 | | - * @return ($fetch is 'object' ? StatsStream|\stdClass|null : ResponseInterface) | | 141 | + * @return ($fetch is 'object' ? StatsStream|object|null : ResponseInterface) | 144 | 142 | */ | 145 | 143 | public function containerStats(string $id, array $queryParameters = [], string $fetch = self::FETCH_OBJECT) | 146 | 144 | { | @@ -270,9 +268,10 @@ public function imagePush(string $name, array $queryParameters = [], array $head | 270 | 268 | * | 271 | 269 | * @return ($fetch is 'object' ? EventStream|null : ResponseInterface) | 272 | 270 | */ | 273 | | - public function systemEvents(array $queryParameters = [], string $fetch = self::FETCH_OBJECT) | | 271 | + public function systemEvents(array $queryParameters = [], string $fetch = self::FETCH_OBJECT, array $accept = []) | 274 | 272 | { | 275 | | - return $this->executeEndpoint(new SystemEvents($queryParameters), $fetch); | | 273 | + // API 1.52 and later accept an Accept header; earlier endpoints ignore it. | | 274 | + return $this->executeEndpoint(new SystemEvents($queryParameters, $accept), $fetch); | 276 | 275 | } | 277 | 276 |
| 278 | 277 | public static function create( | @@ -287,6 +286,10 @@ public static function create( | 287 | 286 | } | 288 | 287 |
| 289 | 288 | $client = parent::create($httpClient, $additionalPlugins, $additionalNormalizers, $applyServerPlugins); | | 289 | + $client->serializer = new Serializer( | | 290 | + [new ArrayDenormalizer(), new JaneObjectNormalizer(), ...$additionalNormalizers], | | 291 | + [new JsonEncoder(new JsonEncode(), new LargeIntegerJsonDecode(['json_decode_associative' => true])), new FormEncoder()] | | 292 | + ); | 290 | 293 | $client->interactiveExecEnabled = $httpClient instanceof InteractiveHttpClient; | 291 | 294 | $response = $client->executeRawEndpoint(new SystemInfo()); | 292 | 295 | $testClient = $response->getBody()->getContents(); | |
Collapse file Copy file name to clipboardExpand all lines: src/DockerClientFactory.php +57-7Lines changed: 57 additions & 7 deletions | Original file line number | Diff line number | Diff line change |
|---|
| 4 | 4 |
| 5 | 5 | namespace Docker; | 6 | 6 |
| | 7 | +use Docker\API\Client as GeneratedClient; | | 8 | +use Docker\Http\ApiVersionNegotiationPlugin; | 7 | 9 | use Docker\Http\InteractiveHttpClient; | 8 | 10 | use Docker\Http\StreamingDecoderPlugin; | 9 | 11 | use Http\Client\Common\Plugin\AddHostPlugin; | | 14 | 16 | use Http\Client\Common\PluginClientFactory; | 15 | 17 | use Http\Client\Socket\Client; | 16 | 18 | use Http\Discovery\Psr17FactoryDiscovery; | | 19 | +use Psr\Http\Client\ClientInterface; | | 20 | +use Psr\Http\Message\RequestInterface; | | 21 | +use Psr\Http\Message\ResponseInterface; | 17 | 22 |
| 18 | 23 | final class DockerClientFactory | 19 | 24 | { | | 25 | + private static ?string $defaultApiVersion = null; | | 26 | + | | 27 | + /** | | 28 | + * The Docker API version of the installed docker-php/docker-php-api package, | | 29 | + * for example "1.56". Requests use it unless api_version or DOCKER_API_VERSION | | 30 | + * says otherwise. | | 31 | + */ | | 32 | + public static function defaultApiVersion(): string | | 33 | + { | | 34 | + if (null !== self::$defaultApiVersion) { | | 35 | + return self::$defaultApiVersion; | | 36 | + } | | 37 | + | | 38 | + // The generated client adds its specification's base path to requests. | | 39 | + // Capture one request instead of trusting Composer metadata, so the | | 40 | + // version always matches the classes that are actually loaded. | | 41 | + $recorder = new class implements ClientInterface { | | 42 | + public ?RequestInterface $request = null; | | 43 | + | | 44 | + public function sendRequest(RequestInterface $request): ResponseInterface | | 45 | + { | | 46 | + $this->request = $request; | | 47 | + | | 48 | + return Psr17FactoryDiscovery::findResponseFactory()->createResponse(204); | | 49 | + } | | 50 | + }; | | 51 | + $client = GeneratedClient::create($recorder); | | 52 | + $httpClient = (fn (): ClientInterface => $this->httpClient)->call($client); | | 53 | + $httpClient->sendRequest(Psr17FactoryDiscovery::findRequestFactory()->createRequest('GET', '/')); | | 54 | + | | 55 | + $path = (string) $recorder->request?->getUri()->getPath(); | | 56 | + if (!preg_match('#^/v([0-9]+\.[0-9]+)/#', $path, $matches)) { | | 57 | + throw new \LogicException('Cannot determine the Docker API version of the installed docker-php/docker-php-api package.'); | | 58 | + } | | 59 | + | | 60 | + return self::$defaultApiVersion = $matches[1]; | | 61 | + } | | 62 | + | 20 | 63 | /** | 21 | | - * The api_version option overrides DOCKER_API_VERSION; other options are | 22 | | - * passed to the socket client. Neither setting changes the generated models. | | 64 | + * The api_version option overrides DOCKER_API_VERSION. Without either, the | | 65 | + * client negotiates: it uses the installed API package's version, or the | | 66 | + * daemon's maximum if that is lower. Other options are passed to the socket | | 67 | + * client. No setting changes the generated models. | 23 | 68 | */ | 24 | 69 | public static function create(array $config = [], ?PluginClientFactory $pluginClientFactory = null): PluginClient | 25 | 70 | { | @@ -30,11 +75,14 @@ public static function create(array $config = [], ?PluginClientFactory $pluginCl | 30 | 75 | $dockerApiVersion = $config['api_version']; | 31 | 76 | unset($config['api_version']); | 32 | 77 | } else { | 33 | | - $dockerApiVersion = getenv('DOCKER_API_VERSION') ?: 'v1.45'; | | 78 | + // Null negotiates with the daemon, up to the installed API package's version. | | 79 | + $dockerApiVersion = getenv('DOCKER_API_VERSION') ?: null; | 34 | 80 | } | 35 | | - $dockerApiVersion = ltrim($dockerApiVersion, '/'); | 36 | | - if (!str_starts_with($dockerApiVersion, 'v')) { | 37 | | - $dockerApiVersion = 'v'.$dockerApiVersion; | | 81 | + if (null !== $dockerApiVersion) { | | 82 | + $dockerApiVersion = ltrim($dockerApiVersion, '/'); | | 83 | + if (!str_starts_with($dockerApiVersion, 'v')) { | | 84 | + $dockerApiVersion = 'v'.$dockerApiVersion; | | 85 | + } | 38 | 86 | } | 39 | 87 |
| 40 | 88 | if (!\array_key_exists('remote_socket', $config)) { | @@ -69,7 +117,9 @@ public static function create(array $config = [], ?PluginClientFactory $pluginCl | 69 | 117 | [ | 70 | 118 | new ContentLengthPlugin(), | 71 | 119 | new StreamingDecoderPlugin(), | 72 | | - new AddPathPlugin($uriFactory->createUri('/'.$dockerApiVersion)), | | 120 | + null === $dockerApiVersion | | 121 | + ? new ApiVersionNegotiationPlugin(self::defaultApiVersion(), $uriFactory, Psr17FactoryDiscovery::findRequestFactory()) | | 122 | + : new AddPathPlugin($uriFactory->createUri('/'.$dockerApiVersion)), | 73 | 123 | new AddHostPlugin($host), | 74 | 124 | new HeaderDefaultsPlugin([ | 75 | 125 | 'host' => $host->withUserInfo('')->getAuthority(), | |
Collapse file Copy file name to clipboardExpand all lines: src/Endpoint/ContainerStats.php +1-1Lines changed: 1 addition & 1 deletion | Original file line number | Diff line number | Diff line change |
|---|
@@ -16,7 +16,7 @@ protected function transformResponseBody(ResponseInterface $response, Serializer | 16 | 16 | // Both modes use application/json, so the request decides: `stream` | 17 | 17 | // keeps the response open and sends one sample per document. | 18 | 18 | if (200 === $response->getStatusCode() && $this->getQueryOptionsResolver()->resolve($this->queryParameters)['stream']) { | 19 | | - return new StatsStream($response->getBody()); | | 19 | + return new StatsStream($response->getBody(), $serializer); | 20 | 20 | } | 21 | 21 |
| 22 | 22 | return parent::transformResponseBody($response, $serializer, $contentType); | |
Collapse file Copy file name to clipboardExpand all lines: src/Endpoint/SystemEvents.php +12Lines changed: 12 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
| 11 | 11 |
| 12 | 12 | class SystemEvents extends BaseEndpoint | 13 | 13 | { | | 14 | + /** | | 15 | + * @param array $accept Accept header values; API 1.52 added this parameter | | 16 | + */ | | 17 | + public function __construct(array $queryParameters = [], array $accept = []) | | 18 | + { | | 19 | + parent::__construct($queryParameters); | | 20 | + // From API 1.52 the generated endpoint stores the Accept values here. | | 21 | + if (property_exists($this, 'accept')) { | | 22 | + $this->accept = $accept; | | 23 | + } | | 24 | + } | | 25 | + | 14 | 26 | protected function transformResponseBody(ResponseInterface $response, SerializerInterface $serializer, ?string $contentType = null) | 15 | 27 | { | 16 | 28 | if (200 === $response->getStatusCode()) { | |
Collapse file Copy file name to clipboardExpand all lines: src/Exception/UnexpectedStatusCodeException.php +1-1Lines changed: 1 addition & 1 deletion | Original file line number | Diff line number | Diff line change |
|---|
| 11 | 11 | /** | 12 | 12 | * Docker returned an error status that the generated endpoint does not handle. | 13 | 13 | * | 14 | | - * Thrown when Docker::throwOnUnexpectedStatus() is enabled; catch | | 14 | + * Thrown unless Docker::throwOnUnexpectedStatus(false) is set; catch | 15 | 15 | * UnexpectedClientErrorException or UnexpectedServerErrorException to tell 4xx | 16 | 16 | * and 5xx responses apart. | 17 | 17 | */ | |
|