From 249744e5625c5ae999bfcc0218bfc94b5df20b80 Mon Sep 17 00:00:00 2001 From: Guillem CANAL Date: Sat, 12 May 2018 19:06:20 +0200 Subject: [PATCH 01/12] feat: Consume Docker system events asynchronously --- src/Client/AmpArtaxStreamEndpoint.php | 26 +++++++ src/Client/AmpArtaxStreamEndpointTrait.php | 41 +++++++++++ src/Client/ProvideAmpArtaxClientOptions.php | 15 ++++ src/DockerAsync.php | 52 ++++++++++++++ src/Endpoint/SystemEvents.php | 13 +++- src/Stream/ArtaxCallbackStream.php | 78 +++++++++++++++++++++ tests/DockerAsyncTest.php | 31 ++++++++ 7 files changed, 255 insertions(+), 1 deletion(-) create mode 100644 src/Client/AmpArtaxStreamEndpoint.php create mode 100644 src/Client/AmpArtaxStreamEndpointTrait.php create mode 100644 src/Client/ProvideAmpArtaxClientOptions.php create mode 100644 src/Stream/ArtaxCallbackStream.php 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 @@ +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/Endpoint/SystemEvents.php b/src/Endpoint/SystemEvents.php index ade82394..712e67da 100644 --- a/src/Endpoint/SystemEvents.php +++ b/src/Endpoint/SystemEvents.php @@ -4,15 +4,26 @@ namespace Docker\Endpoint; +use Amp\Artax\Client as ArtaxClient; use Docker\API\Endpoint\SystemEvents as BaseEndpoint; +use Docker\Client\AmpArtaxStreamEndpoint; +use Docker\Client\AmpArtaxStreamEndpointTrait; +use Docker\Client\ProvideAmpArtaxClientOptions; use Docker\Stream\EventStream; use Jane\OpenApiRuntime\Client\Client; use Jane\OpenApiRuntime\Client\Exception\InvalidFetchModeException; use Psr\Http\Message\ResponseInterface; use Symfony\Component\Serializer\SerializerInterface; -class SystemEvents extends BaseEndpoint +class SystemEvents extends BaseEndpoint implements ProvideAmpArtaxClientOptions, AmpArtaxStreamEndpoint { + use AmpArtaxStreamEndpointTrait; + + public function getAmpArtaxClientOptions(): array + { + return [ArtaxClient::OP_TRANSFER_TIMEOUT => 0]; + } + public function parsePSR7Response(ResponseInterface $response, SerializerInterface $serializer, string $fetchMode = Client::FETCH_OBJECT) { if (Client::FETCH_OBJECT === $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/tests/DockerAsyncTest.php b/tests/DockerAsyncTest.php index 2fd15108..0d854511 100644 --- a/tests/DockerAsyncTest.php +++ b/tests/DockerAsyncTest.php @@ -4,8 +4,10 @@ namespace Docker\Tests; +use Amp\Delayed; use Amp\Loop; use Docker\API\Model\ContainersCreatePostBody; +use Docker\API\Model\EventsGetResponse200; use Docker\DockerAsync; class DockerAsyncTest extends \PHPUnit\Framework\TestCase @@ -39,4 +41,33 @@ public function testAsync(): void $this->assertSame($containerCreate->getId(), $containerInfo->getId()); }); } + + public function testSystemEventsAllowTheConsumptionOfDockerEvents(): void + { + Loop::run(function () { + $docker = DockerAsync::create(); + + $actualEvent = null; + + $events = yield $docker->systemEvents(); + $events->onFrame(function ($event) use (&$actualEvent): void { + $actualEvent = $event; + }); + $events->listen(); + + $containerConfig = new ContainersCreatePostBody(); + $containerConfig->setImage('busybox:latest'); + $containerConfig->setCmd(['echo', '-n', 'output']); + + $containerCreate = yield $docker->containerCreate($containerConfig); + + // Let a chance for the container create event to be dispatched to the consumer + yield new Delayed(100); + + $events->cancel(); + + $this->assertInstanceOf(EventsGetResponse200::class, $actualEvent); + $this->assertSame($actualEvent->getActor()->getId(), $containerCreate->getId()); + }); + } } From 0b3cdcb3401fe583e9e9a4acf140f371e09c87e9 Mon Sep 17 00:00:00 2001 From: Guillem CANAL Date: Sat, 12 May 2018 21:13:42 +0200 Subject: [PATCH 02/12] fix: Increase the delay between the container creation and the phpunit assertions --- tests/DockerAsyncTest.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/DockerAsyncTest.php b/tests/DockerAsyncTest.php index 0d854511..1c9e198e 100644 --- a/tests/DockerAsyncTest.php +++ b/tests/DockerAsyncTest.php @@ -9,6 +9,7 @@ use Docker\API\Model\ContainersCreatePostBody; use Docker\API\Model\EventsGetResponse200; use Docker\DockerAsync; +use Docker\Stream\ArtaxCallbackStream; class DockerAsyncTest extends \PHPUnit\Framework\TestCase { @@ -48,7 +49,7 @@ public function testSystemEventsAllowTheConsumptionOfDockerEvents(): void $docker = DockerAsync::create(); $actualEvent = null; - + /** @var ArtaxCallbackStream $events */ $events = yield $docker->systemEvents(); $events->onFrame(function ($event) use (&$actualEvent): void { $actualEvent = $event; @@ -62,7 +63,7 @@ public function testSystemEventsAllowTheConsumptionOfDockerEvents(): void $containerCreate = yield $docker->containerCreate($containerConfig); // Let a chance for the container create event to be dispatched to the consumer - yield new Delayed(100); + yield new Delayed(1000); $events->cancel(); From f98e572950ce85ae98d89a89379f8bbce6869b4b Mon Sep 17 00:00:00 2001 From: Guillem CANAL Date: Sat, 12 May 2018 21:30:53 +0200 Subject: [PATCH 03/12] fix: Only listen to Docker container create event --- tests/DockerAsyncTest.php | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/tests/DockerAsyncTest.php b/tests/DockerAsyncTest.php index 1c9e198e..1c231baa 100644 --- a/tests/DockerAsyncTest.php +++ b/tests/DockerAsyncTest.php @@ -48,11 +48,15 @@ public function testSystemEventsAllowTheConsumptionOfDockerEvents(): void Loop::run(function () { $docker = DockerAsync::create(); - $actualEvent = null; + $receivedEvents = []; /** @var ArtaxCallbackStream $events */ - $events = yield $docker->systemEvents(); - $events->onFrame(function ($event) use (&$actualEvent): void { - $actualEvent = $event; + $events = yield $docker->systemEvents( + [ + 'filters' => json_encode(['type' => ['container'], 'action' => ['create']]) + ] + ); + $events->onFrame(function ($event) use (&$receivedEvents): void { + $receivedEvents[] = $event; }); $events->listen(); @@ -67,8 +71,9 @@ public function testSystemEventsAllowTheConsumptionOfDockerEvents(): void $events->cancel(); - $this->assertInstanceOf(EventsGetResponse200::class, $actualEvent); - $this->assertSame($actualEvent->getActor()->getId(), $containerCreate->getId()); + $this->assertCount(1, $receivedEvents); + $this->assertInstanceOf(EventsGetResponse200::class, current($receivedEvents)); + $this->assertSame(current($receivedEvents)->getActor()->getId(), $containerCreate->getId()); }); } } From 9e7857c2ff596f6decba4b89dd7d0206e18ff88a Mon Sep 17 00:00:00 2001 From: Guillem CANAL Date: Sat, 12 May 2018 21:55:05 +0200 Subject: [PATCH 04/12] fix: attempt to catch the proper event using a macther (docker may not run in isolation in Travis CI) --- tests/DockerAsyncTest.php | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/tests/DockerAsyncTest.php b/tests/DockerAsyncTest.php index 1c231baa..5ea5c13c 100644 --- a/tests/DockerAsyncTest.php +++ b/tests/DockerAsyncTest.php @@ -7,6 +7,7 @@ use Amp\Delayed; use Amp\Loop; use Docker\API\Model\ContainersCreatePostBody; +use Docker\API\Model\ContainersCreatePostResponse201; use Docker\API\Model\EventsGetResponse200; use Docker\DockerAsync; use Docker\Stream\ArtaxCallbackStream; @@ -64,6 +65,7 @@ public function testSystemEventsAllowTheConsumptionOfDockerEvents(): void $containerConfig->setImage('busybox:latest'); $containerConfig->setCmd(['echo', '-n', 'output']); + /** @var ContainersCreatePostResponse201 $containerCreate */ $containerCreate = yield $docker->containerCreate($containerConfig); // Let a chance for the container create event to be dispatched to the consumer @@ -71,9 +73,19 @@ public function testSystemEventsAllowTheConsumptionOfDockerEvents(): void $events->cancel(); - $this->assertCount(1, $receivedEvents); - $this->assertInstanceOf(EventsGetResponse200::class, current($receivedEvents)); - $this->assertSame(current($receivedEvents)->getActor()->getId(), $containerCreate->getId()); + $matchedEvents = array_filter( + $receivedEvents, + function ($event) use ($containerCreate) { + return \is_object($event) + && $event instanceof EventsGetResponse200 + && $event->getAction() === 'create' + && $event->getType() === 'container' + && $event->getActor() !== null + && $event->getActor()->getID() === $containerCreate->getId(); + } + ); + + $this->assertCount(1, $matchedEvents); }); } } From f072ecf38473383ba5e372d56ea632815b3ce9d6 Mon Sep 17 00:00:00 2001 From: Guillem CANAL Date: Sat, 12 May 2018 22:19:09 +0200 Subject: [PATCH 05/12] fix: Crazy formatting requirements in tests --- tests/DockerAsyncTest.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/DockerAsyncTest.php b/tests/DockerAsyncTest.php index 5ea5c13c..c04a281a 100644 --- a/tests/DockerAsyncTest.php +++ b/tests/DockerAsyncTest.php @@ -53,7 +53,7 @@ public function testSystemEventsAllowTheConsumptionOfDockerEvents(): void /** @var ArtaxCallbackStream $events */ $events = yield $docker->systemEvents( [ - 'filters' => json_encode(['type' => ['container'], 'action' => ['create']]) + 'filters' => \json_encode(['type' => ['container'], 'action' => ['create']]), ] ); $events->onFrame(function ($event) use (&$receivedEvents): void { @@ -73,14 +73,14 @@ public function testSystemEventsAllowTheConsumptionOfDockerEvents(): void $events->cancel(); - $matchedEvents = array_filter( + $matchedEvents = \array_filter( $receivedEvents, function ($event) use ($containerCreate) { return \is_object($event) && $event instanceof EventsGetResponse200 - && $event->getAction() === 'create' - && $event->getType() === 'container' - && $event->getActor() !== null + && 'create' === $event->getAction() + && 'container' === $event->getType() + && null !== $event->getActor() && $event->getActor()->getID() === $containerCreate->getId(); } ); From d7bdc16654eca91ee22b04913052eb5aafe67481 Mon Sep 17 00:00:00 2001 From: Guillem CANAL Date: Sun, 13 May 2018 20:34:45 +0200 Subject: [PATCH 06/12] fix: Use Loop::delay instead of a Delayed callback --- tests/DockerAsyncTest.php | 59 +++++++++++++++++++-------------------- 1 file changed, 28 insertions(+), 31 deletions(-) diff --git a/tests/DockerAsyncTest.php b/tests/DockerAsyncTest.php index c04a281a..0cda17c5 100644 --- a/tests/DockerAsyncTest.php +++ b/tests/DockerAsyncTest.php @@ -4,10 +4,8 @@ namespace Docker\Tests; -use Amp\Delayed; use Amp\Loop; use Docker\API\Model\ContainersCreatePostBody; -use Docker\API\Model\ContainersCreatePostResponse201; use Docker\API\Model\EventsGetResponse200; use Docker\DockerAsync; use Docker\Stream\ArtaxCallbackStream; @@ -44,48 +42,47 @@ public function testAsync(): void }); } + /** @group sys */ public function testSystemEventsAllowTheConsumptionOfDockerEvents(): void { - Loop::run(function () { + \putenv('AMP_DEBUG=1'); + $matchedEvents = []; + + Loop::run(function () use (&$matchedEvents) { $docker = DockerAsync::create(); - $receivedEvents = []; /** @var ArtaxCallbackStream $events */ - $events = yield $docker->systemEvents( - [ - 'filters' => \json_encode(['type' => ['container'], 'action' => ['create']]), - ] - ); - $events->onFrame(function ($event) use (&$receivedEvents): void { - $receivedEvents[] = $event; + $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']); - /** @var ContainersCreatePostResponse201 $containerCreate */ - $containerCreate = yield $docker->containerCreate($containerConfig); - - // Let a chance for the container create event to be dispatched to the consumer - yield new Delayed(1000); + yield $docker->containerCreate($containerConfig); - $events->cancel(); - - $matchedEvents = \array_filter( - $receivedEvents, - function ($event) use ($containerCreate) { - return \is_object($event) - && $event instanceof EventsGetResponse200 - && 'create' === $event->getAction() - && 'container' === $event->getType() - && null !== $event->getActor() - && $event->getActor()->getID() === $containerCreate->getId(); - } - ); - - $this->assertCount(1, $matchedEvents); + Loop::delay(1000, function () use ($events): void { + $events->cancel(); + }); }); + + $this->assertCount(1, $matchedEvents); } } From 73c0949843c29c076fb84147de86b7b9a9f8f1d3 Mon Sep 17 00:00:00 2001 From: Guillem CANAL Date: Sun, 13 May 2018 23:46:13 +0200 Subject: [PATCH 07/12] fix: Replaced $events->cancel() by Loop::stop() Reason: The travis step responsible to generate the coverage report is doing a composer update --prefer-stable --prefer-lowest --no-interaction which retreive an unstable version of amphp/artax. please check https://github.com/amphp/artax/blob/master/CHANGELOG.md#306 --- tests/DockerAsyncTest.php | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/tests/DockerAsyncTest.php b/tests/DockerAsyncTest.php index 0cda17c5..29b2f3bf 100644 --- a/tests/DockerAsyncTest.php +++ b/tests/DockerAsyncTest.php @@ -42,10 +42,8 @@ public function testAsync(): void }); } - /** @group sys */ public function testSystemEventsAllowTheConsumptionOfDockerEvents(): void { - \putenv('AMP_DEBUG=1'); $matchedEvents = []; Loop::run(function () use (&$matchedEvents) { @@ -78,8 +76,8 @@ public function testSystemEventsAllowTheConsumptionOfDockerEvents(): void yield $docker->containerCreate($containerConfig); - Loop::delay(1000, function () use ($events): void { - $events->cancel(); + Loop::delay(1000, function (): void { + Loop::stop(); }); }); From 2a6df3d431e16a0b153cec1df8247b1031b3e72a Mon Sep 17 00:00:00 2001 From: Dibyajyoti Panda Date: Mon, 18 Jun 2018 02:42:49 +0530 Subject: [PATCH 08/12] Update container-run.md Added an example for executing command on running container. --- docs/cookbook/container-run.md | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/docs/cookbook/container-run.md b/docs/cookbook/container-run.md index 0820e80a..6876d79e 100644 --- a/docs/cookbook/container-run.md +++ b/docs/cookbook/container-run.md @@ -208,3 +208,29 @@ $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); +$output = $docker->execStart($execid,$execStartConfig); + +var_dump($output); +``` From bbc493426d853581bc4aa73dc95b323fb05606d7 Mon Sep 17 00:00:00 2001 From: Dibyajyoti Panda Date: Mon, 18 Jun 2018 03:14:54 +0530 Subject: [PATCH 09/12] Update container-run.md --- docs/cookbook/container-run.md | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/docs/cookbook/container-run.md b/docs/cookbook/container-run.md index 6876d79e..2ee824bf 100644 --- a/docs/cookbook/container-run.md +++ b/docs/cookbook/container-run.md @@ -230,7 +230,22 @@ $execConfig->setCmd(['mkdir', '/tmp/testDir']); $execid = $docker->containerExec('android',$execConfig)->getId(); $execStartConfig = new ExecIdStartPostBody(); $execStartConfig->setDetach(false); -$output = $docker->execStart($execid,$execStartConfig); -var_dump($output); +// 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 ]) ; ``` From 885695f2f77be3727293664ad602ef16188f0c91 Mon Sep 17 00:00:00 2001 From: philip keighley Date: Mon, 9 Jul 2018 18:28:18 -0400 Subject: [PATCH 10/12] Fix grammatical error --- docs/cookbook/build-image.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/cookbook/build-image.md b/docs/cookbook/build-image.md index e412361f..bea40589 100644 --- a/docs/cookbook/build-image.md +++ b/docs/cookbook/build-image.md @@ -37,7 +37,7 @@ $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 too you handle +The build function will return the raw [PSR7](http://www.php-fig.org/psr/psr-7/) Response. It's up to you handle decoding and receiving correct output in this case. ## Context From 909d1c2d73284645035ff971cef814917f9a1997 Mon Sep 17 00:00:00 2001 From: Philip Keighley Date: Fri, 28 Dec 2018 09:39:17 -0500 Subject: [PATCH 11/12] Add missing 'to' before 'handle' --- docs/cookbook/build-image.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/cookbook/build-image.md b/docs/cookbook/build-image.md index bea40589..71a5d4bc 100644 --- a/docs/cookbook/build-image.md +++ b/docs/cookbook/build-image.md @@ -37,7 +37,7 @@ $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 handle +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 From c44f1f2f288cef810abbb1288f9835dfeaddb929 Mon Sep 17 00:00:00 2001 From: Joel Wurtz Date: Fri, 25 Oct 2019 20:36:58 +0200 Subject: [PATCH 12/12] No longer maintained --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 94092f39..27464be6 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,7 @@ +# No longer maintained + +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). + Docker PHP ==========