From afafc9471164c909a9eead5c917b7b7ad12f00c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Tue, 26 Nov 2024 16:53:49 +0100 Subject: [PATCH 1/2] Update to construct URLs without dependency on `rize/uri-template` --- composer.json | 3 +- src/Client.php | 502 ++++++++++++++++--------------------------- tests/ClientTest.php | 23 +- 3 files changed, 213 insertions(+), 315 deletions(-) diff --git a/composer.json b/composer.json index 9642264..5782f56 100644 --- a/composer.json +++ b/composer.json @@ -18,8 +18,7 @@ "react/promise": "^3.3 || ^2.11 || ^1.3", "react/promise-stream": "^1.6", "react/socket": "^1.17", - "react/stream": "^1.4", - "rize/uri-template": "^0.4.1 || ^0.3" + "react/stream": "^1.4" }, "require-dev": { "clue/tar-react": "^0.2", diff --git a/src/Client.php b/src/Client.php index a11dc2f..61d5c92 100644 --- a/src/Client.php +++ b/src/Client.php @@ -10,7 +10,6 @@ use React\Socket\FixedUriConnector; use React\Socket\UnixConnector; use React\Stream\ReadableStreamInterface; -use Rize\UriTemplate; /** * The Docker Engine API client can be used to control your (local) Docker daemon. @@ -31,7 +30,6 @@ class Client private $browser; private $parser; private $streamingParser; - private $uri; /** * @@ -87,7 +85,6 @@ public function __construct($loop = null, $url = null) $this->browser = $browser->withBase($url); $this->parser = new ResponseParser(); $this->streamingParser = new StreamingParser(); - $this->uri = new UriTemplate(); } /** @@ -98,6 +95,7 @@ public function __construct($loop = null, $url = null) */ public function ping() { + // GET /_ping return $this->browser->get('_ping')->then(array($this->parser, 'expectPlain')); } @@ -109,6 +107,7 @@ public function ping() */ public function info() { + // GET /info return $this->browser->get('info')->then(array($this->parser, 'expectJson')); } @@ -120,6 +119,7 @@ public function info() */ public function version() { + // GET /version return $this->browser->get('version')->then(array($this->parser, 'expectJson')); } @@ -188,17 +188,15 @@ public function events($since = null, $until = null, $filters = array()) */ public function eventsStream($since = null, $until = null, $filters = array()) { + // GET /events{?since,until,filters} return $this->streamingParser->parseJsonStream( $this->browser->requestStreaming( 'GET', - $this->uri->expand( - 'events{?since,until,filters}', - array( - 'since' => $since, - 'until' => $until, - 'filters' => $filters ? json_encode($filters) : null - ) - ) + 'events' . $this->queryString(array( + 'since' => $since, + 'until' => $until, + 'filters' => $filters ? json_encode($filters) : null + )) ) ); } @@ -213,14 +211,12 @@ public function eventsStream($since = null, $until = null, $filters = array()) */ public function containerList($all = false, $size = false) { + // GET /containers/json{?all,size} return $this->browser->get( - $this->uri->expand( - 'containers/json{?all,size}', - array( - 'all' => $this->boolArg($all), - 'size' => $this->boolArg($size) - ) - ) + 'containers/json' . $this->queryString(array( + 'all' => $this->boolArg($all), + 'size' => $this->boolArg($size) + )) )->then(array($this->parser, 'expectJson')); } @@ -234,13 +230,11 @@ public function containerList($all = false, $size = false) */ public function containerCreate($config, $name = null) { + // POST /containers/create{?name} return $this->postJson( - $this->uri->expand( - 'containers/create{?name}', - array( - 'name' => $name - ) - ), + 'containers/create' . $this->queryString(array( + 'name' => $name + )), $config )->then(array($this->parser, 'expectJson')); } @@ -254,13 +248,9 @@ public function containerCreate($config, $name = null) */ public function containerInspect($container) { + // GET /containers/{container}/json return $this->browser->get( - $this->uri->expand( - 'containers/{container}/json', - array( - 'container' => $container - ) - ) + 'containers/' . rawurlencode($container) . '/json' )->then(array($this->parser, 'expectJson')); } @@ -274,14 +264,11 @@ public function containerInspect($container) */ public function containerTop($container, $ps_args = null) { + // GET /containers/{container}/top{?ps_args} return $this->browser->get( - $this->uri->expand( - 'containers/{container}/top{?ps_args}', - array( - 'container' => $container, - 'ps_args' => $ps_args - ) - ) + 'containers/' . rawurlencode($container) . '/top' . $this->queryString(array( + 'ps_args' => $ps_args + )) )->then(array($this->parser, 'expectJson')); } @@ -366,18 +353,16 @@ public function containerLogsStream($container, $follow = false, $stdout = true, { $parser = $this->streamingParser; $browser = $this->browser; - $url = $this->uri->expand( - 'containers/{container}/logs{?follow,stdout,stderr,since,timestamps,tail}', - array( - 'container' => $container, - 'follow' => $this->boolArg($follow), - 'stdout' => $this->boolArg($stdout), - 'stderr' => $this->boolArg($stderr), - 'since' => ($since === 0) ? null : $since, - 'timestamps' => $this->boolArg($timestamps), - 'tail' => $tail - ) - ); + + // GET /containers/{container}/logs{?follow,stdout,stderr,since,timestamps,tail} + $url = 'containers/' . rawurlencode($container) . '/logs' . $this->queryString(array( + 'follow' => $this->boolArg($follow), + 'stdout' => $this->boolArg($stdout), + 'stderr' => $this->boolArg($stderr), + 'since' => ($since === 0) ? null : $since, + 'timestamps' => $this->boolArg($timestamps), + 'tail' => $tail + )); // first inspect container to check TTY setting, then request logs with appropriate log parser return \React\Promise\Stream\unwrapReadable($this->containerInspect($container)->then(function ($info) use ($url, $browser, $parser, $stderrEvent) { @@ -400,13 +385,9 @@ public function containerLogsStream($container, $follow = false, $stdout = true, */ public function containerChanges($container) { + // GET /containers/{container}/changes return $this->browser->get( - $this->uri->expand( - 'containers/{container}/changes', - array( - 'container' => $container - ) - ) + 'containers/' . rawurlencode($container) . '/changes' )->then(array($this->parser, 'expectJson')); } @@ -432,13 +413,9 @@ public function containerChanges($container) */ public function containerExport($container) { + // GET /containers/{container}/export return $this->browser->get( - $this->uri->expand( - 'containers/{container}/export', - array( - 'container' => $container - ) - ) + 'containers/' . rawurlencode($container) . '/export' )->then(array($this->parser, 'expectPlain')); } @@ -466,15 +443,11 @@ public function containerExport($container) */ public function containerExportStream($container) { + // GET /containers/{container}/export return $this->streamingParser->parsePlainStream( $this->browser->requestStreaming( 'GET', - $this->uri->expand( - 'containers/{container}/export', - array( - 'container' => $container - ) - ) + 'containers/' . rawurlencode($container) . '/export' ) ); } @@ -496,14 +469,11 @@ public function containerExportStream($container) */ public function containerStats($container) { + // GET /containers/{container}/stats{?stream} return $this->browser->get( - $this->uri->expand( - 'containers/{container}/stats{?stream}', - array( - 'container' => $container, - 'stream' => 0 - ) - ) + 'containers/' . rawurlencode($container) . '/stats' . $this->queryString(array( + 'stream' => 0 + )) )->then(array($this->parser, 'expectJson')); } @@ -524,15 +494,11 @@ public function containerStats($container) */ public function containerStatsStream($container) { + // GET /containers/{container}/stats return $this->streamingParser->parseJsonStream( $this->browser->requestStreaming( 'GET', - $this->uri->expand( - 'containers/{container}/stats', - array( - 'container' => $container - ) - ) + 'containers/' . rawurlencode($container) . '/stats' ) ); } @@ -548,15 +514,12 @@ public function containerStatsStream($container) */ public function containerResize($container, $w, $h) { + // POST /containers/{container}/resize{?w,h} return $this->browser->post( - $this->uri->expand( - 'containers/{container}/resize{?w,h}', - array( - 'container' => $container, - 'w' => $w, - 'h' => $h, - ) - ) + 'containers/' . rawurlencode($container) . '/resize' . $this->queryString(array( + 'w' => $w, + 'h' => $h + )) )->then(array($this->parser, 'expectEmpty')); } @@ -569,13 +532,9 @@ public function containerResize($container, $w, $h) */ public function containerStart($container) { + // POST /containers/{container}/start return $this->browser->post( - $this->uri->expand( - 'containers/{container}/start', - array( - 'container' => $container - ) - ) + 'containers/' . rawurlencode($container) . '/start' )->then(array($this->parser, 'expectEmpty')); } @@ -589,14 +548,11 @@ public function containerStart($container) */ public function containerStop($container, $t = null) { + // POST /containers/{container}/stop{?t} return $this->browser->post( - $this->uri->expand( - 'containers/{container}/stop{?t}', - array( - 'container' => $container, - 't' => $t - ) - ) + 'containers/' . rawurlencode($container) . '/stop' . $this->queryString(array( + 't' => $t + )) )->then(array($this->parser, 'expectEmpty')); } @@ -610,14 +566,11 @@ public function containerStop($container, $t = null) */ public function containerRestart($container, $t = null) { + // POST /containers/{container}/restart{?t} return $this->browser->post( - $this->uri->expand( - 'containers/{container}/restart{?t}', - array( - 'container' => $container, - 't' => $t - ) - ) + 'containers/' . rawurlencode($container) . '/restart' . $this->queryString(array( + 't' => $t + )) )->then(array($this->parser, 'expectEmpty')); } @@ -631,14 +584,11 @@ public function containerRestart($container, $t = null) */ public function containerKill($container, $signal = null) { + // POST /containers/{container}/kill{?signal} return $this->browser->post( - $this->uri->expand( - 'containers/{container}/kill{?signal}', - array( - 'container' => $container, - 'signal' => $signal - ) - ) + 'containers/' . rawurlencode($container) . '/kill' . $this->queryString(array( + 'signal' => $signal + )) )->then(array($this->parser, 'expectEmpty')); } @@ -654,14 +604,11 @@ public function containerKill($container, $signal = null) */ public function containerRename($container, $name) { + // POST /containers/{container}/rename{?name} return $this->browser->post( - $this->uri->expand( - 'containers/{container}/rename{?name}', - array( - 'container' => $container, - 'name' => $name - ) - ) + 'containers/' . rawurlencode($container) . '/rename' . $this->queryString(array( + 'name' => $name + )) )->then(array($this->parser, 'expectEmpty')); } @@ -674,13 +621,9 @@ public function containerRename($container, $name) */ public function containerPause($container) { + // POST /containers/{container}/pause return $this->browser->post( - $this->uri->expand( - 'containers/{container}/pause', - array( - 'container' => $container - ) - ) + 'containers/' . rawurlencode($container) . '/pause' )->then(array($this->parser, 'expectEmpty')); } @@ -693,13 +636,9 @@ public function containerPause($container) */ public function containerUnpause($container) { + // POST /containers/{container}/unpause return $this->browser->post( - $this->uri->expand( - 'containers/{container}/unpause', - array( - 'container' => $container - ) - ) + 'containers/' . rawurlencode($container) . '/unpause' )->then(array($this->parser, 'expectEmpty')); } @@ -780,16 +719,14 @@ public function containerAttachStream($container, $logs = false, $stream = false { $parser = $this->streamingParser; $browser = $this->browser; - $url = $this->uri->expand( - 'containers/{container}/attach{?logs,stream,stdout,stderr}', - array( - 'container' => $container, - 'logs' => $this->boolArg($logs), - 'stream' => $this->boolArg($stream), - 'stdout' => $this->boolArg($stdout), - 'stderr' => $this->boolArg($stderr) - ) - ); + + // POST /containers/{container}/attach{?logs,stream,stdout,stderr} + $url = 'containers/' . rawurlencode($container) . '/attach' . $this->queryString(array( + 'logs' => $this->boolArg($logs), + 'stream' => $this->boolArg($stream), + 'stdout' => $this->boolArg($stdout), + 'stderr' => $this->boolArg($stderr) + )); // first inspect container to check TTY setting, then attach with appropriate log parser return \React\Promise\Stream\unwrapReadable($this->containerInspect($container)->then(function ($info) use ($url, $browser, $parser, $stderrEvent) { @@ -813,14 +750,11 @@ public function containerAttachStream($container, $logs = false, $stream = false */ public function containerWait($container, $condition = null) { + // POST /containers/{container}/wait{?condition} return $this->browser->post( - $this->uri->expand( - 'containers/{container}/wait{?condition}', - array( - 'container' => $container, - 'condition' => $condition - ) - ) + 'containers/' . rawurlencode($container) . '/wait' . $this->queryString(array( + 'condition' => $condition + )) )->then(array($this->parser, 'expectJson')); } @@ -835,15 +769,12 @@ public function containerWait($container, $condition = null) */ public function containerRemove($container, $v = false, $force = false) { + // DELETE /containers/{container}{?v,force} return $this->browser->delete( - $this->uri->expand( - 'containers/{container}{?v,force}', - array( - 'container' => $container, - 'v' => $this->boolArg($v), - 'force' => $this->boolArg($force) - ) - ) + 'containers/' . rawurlencode($container) . $this->queryString(array( + 'v' => $this->boolArg($v), + 'force' => $this->boolArg($force) + )) )->then(array($this->parser, 'expectEmpty')); } @@ -872,14 +803,11 @@ public function containerRemove($container, $v = false, $force = false) */ public function containerArchive($container, $path) { + // GET /containers/{container}/archive{?path} return $this->browser->get( - $this->uri->expand( - 'containers/{container}/archive{?path}', - array( - 'container' => $container, - 'path' => $path - ) - ) + 'containers/' . rawurlencode($container) . '/archive' . $this->queryString(array( + 'path' => $path + )) )->then(array($this->parser, 'expectPlain')); } @@ -910,16 +838,13 @@ public function containerArchive($container, $path) */ public function containerArchiveStream($container, $path) { + // GET /containers/{container}/archive{?path} return $this->streamingParser->parsePlainStream( $this->browser->requestStreaming( 'GET', - $this->uri->expand( - 'containers/{container}/archive{?path}', - array( - 'container' => $container, - 'path' => $path - ) - ) + 'containers/' . rawurlencode($container) . '/archive' . $this->queryString(array( + 'path' => $path + )) ) ); } @@ -934,13 +859,11 @@ public function containerArchiveStream($container, $path) */ public function imageList($all = false) { + // GET /images/json{?all} return $this->browser->get( - $this->uri->expand( - 'images/json{?all}', - array( - 'all' => $this->boolArg($all) - ) - ) + 'images/json' . $this->queryString(array( + 'all' => $this->boolArg($all) + )) )->then(array($this->parser, 'expectJson')); } @@ -1001,19 +924,17 @@ public function imageCreate($fromImage = null, $fromSrc = null, $repo = null, $t */ public function imageCreateStream($fromImage = null, $fromSrc = null, $repo = null, $tag = null, $registry = null, $registryAuth = null) { + // POST /images/create{?fromImage,fromSrc,repo,tag,registry} return $this->streamingParser->parseJsonStream( $this->browser->requestStreaming( 'POST', - $this->uri->expand( - 'images/create{?fromImage,fromSrc,repo,tag,registry}', - array( - 'fromImage' => $fromImage, - 'fromSrc' => $fromSrc, - 'repo' => $repo, - 'tag' => $tag, - 'registry' => $registry - ) - ), + 'images/create' . $this->queryString(array( + 'fromImage' => $fromImage, + 'fromSrc' => $fromSrc, + 'repo' => $repo, + 'tag' => $tag, + 'registry' => $registry + )), $this->authHeaders($registryAuth) ) ); @@ -1028,13 +949,9 @@ public function imageCreateStream($fromImage = null, $fromSrc = null, $repo = nu */ public function imageInspect($image) { + // GET /images/{image}/json return $this->browser->get( - $this->uri->expand( - 'images/{image}/json', - array( - 'image' => $image - ) - ) + 'images/' . rawurlencode($image) . '/json' )->then(array($this->parser, 'expectJson')); } @@ -1047,13 +964,9 @@ public function imageInspect($image) */ public function imageHistory($image) { + // GET /images/{image}/history return $this->browser->get( - $this->uri->expand( - 'images/{image}/history', - array( - 'image' => $image - ) - ) + 'images/' . rawurlencode($image) . '/history' )->then(array($this->parser, 'expectJson')); } @@ -1109,17 +1022,13 @@ public function imagePush($image, $tag = null, $registry = null, $registryAuth = */ public function imagePushStream($image, $tag = null, $registry = null, $registryAuth = null) { + // POST /images{/registry}/{image}/push{?tag} return $this->streamingParser->parseJsonStream( $this->browser->requestStreaming( 'POST', - $this->uri->expand( - 'images{/registry}/{image}/push{?tag}', - array( - 'registry' => $registry, - 'image' => $image, - 'tag' => $tag - ) - ), + 'images' . ($registry !== null ? '/' . rawurlencode($registry) : '') . '/' . rawurlencode($image) . '/push' . $this->queryString(array( + 'tag' => $tag + )), $this->authHeaders($registryAuth) ) ); @@ -1137,16 +1046,13 @@ public function imagePushStream($image, $tag = null, $registry = null, $registry */ public function imageTag($image, $repo, $tag = null, $force = false) { + // POST /images/{image}/tag{?repo,tag,force} return $this->browser->post( - $this->uri->expand( - 'images/{image}/tag{?repo,tag,force}', - array( - 'image' => $image, - 'repo' => $repo, - 'tag' => $tag, - 'force' => $this->boolArg($force) - ) - ) + 'images/' . rawurlencode($image) . '/tag' . $this->queryString(array( + 'repo' => $repo, + 'tag' => $tag, + 'force' => $this->boolArg($force) + )) )->then(array($this->parser, 'expectEmpty')); } @@ -1161,15 +1067,12 @@ public function imageTag($image, $repo, $tag = null, $force = false) */ public function imageRemove($image, $force = false, $noprune = false) { + // DELETE /images/{image}{?force,noprune} return $this->browser->delete( - $this->uri->expand( - 'images/{image}{?force,noprune}', - array( - 'image' => $image, - 'force' => $this->boolArg($force), - 'noprune' => $this->boolArg($noprune) - ) - ) + 'images/' . rawurlencode($image) . $this->queryString(array( + 'force' => $this->boolArg($force), + 'noprune' => $this->boolArg($noprune) + )) )->then(array($this->parser, 'expectEmpty')); } @@ -1182,13 +1085,11 @@ public function imageRemove($image, $force = false, $noprune = false) */ public function imageSearch($term) { + // GET /images/search{?term} return $this->browser->get( - $this->uri->expand( - 'images/search{?term}', - array( - 'term' => $term - ) - ) + 'images/search' . $this->queryString(array( + 'term' => $term + )) )->then(array($this->parser, 'expectJson')); } @@ -1208,19 +1109,17 @@ public function imageSearch($term) */ public function containerCommit($container, $repo = null, $tag = null, $comment = null, $author = null, $pause = true, $changes = null, array $config = array()) { + // POST /commit{?container,repo,tag,comment,author,pause,changes} return $this->postJson( - $this->uri->expand( - 'commit{?container,repo,tag,comment,author,pause,changes}', - array( - 'container' => $container, - 'repo' => $repo, - 'tag' => $tag, - 'comment' => $comment, - 'author' => $author, - 'pause' => $pause ? 1 : 0, - 'changes' => $changes, - ) - ), + 'commit' . $this->queryString(array( + 'container' => $container, + 'repo' => $repo, + 'tag' => $tag, + 'comment' => $comment, + 'author' => $author, + 'pause' => $pause ? 1 : 0, + 'changes' => $changes + )), $config )->then(array($this->parser, 'expectJson')); } @@ -1267,13 +1166,9 @@ public function execCreate($container, $cmd, $tty = false, $stdin = false, $stdo $cmd = array('sh', '-c', (string)$cmd); } + // POST /containers/{container}/exec return $this->postJson( - $this->uri->expand( - 'containers/{container}/exec', - array( - 'container' => $container - ) - ), + 'containers/' . rawurlencode($container) . '/exec', array( 'Cmd' => $cmd, 'Tty' => !!$tty, @@ -1327,13 +1222,9 @@ public function execStart($exec, $tty = false) */ public function execStartDetached($exec, $tty = false) { + // POST /exec/{exec}/start return $this->browser->post( - $this->uri->expand( - 'exec/{exec}/start', - array( - 'exec' => $exec - ) - ), + 'exec/' . rawurlencode($exec) . '/start', array( 'Content-Type' => 'application/json' ), @@ -1371,15 +1262,11 @@ public function execStartDetached($exec, $tty = false) */ public function execStartStream($exec, $tty = false, $stderrEvent = null) { + // POST /exec/{exec}/start $stream = $this->streamingParser->parsePlainStream( $this->browser->requestStreaming( 'POST', - $this->uri->expand( - 'exec/{exec}/start', - array( - 'exec' => $exec - ) - ), + 'exec/' . rawurlencode($exec) . '/start', array( 'Content-Type' => 'application/json' ), @@ -1410,15 +1297,12 @@ public function execStartStream($exec, $tty = false, $stderrEvent = null) */ public function execResize($exec, $w, $h) { + // POST /exec/{exec}/resize{?w,h} return $this->browser->post( - $this->uri->expand( - 'exec/{exec}/resize{?w,h}', - array( - 'exec' => $exec, - 'w' => $w, - 'h' => $h - ) - ) + 'exec/' . rawurlencode($exec) . '/resize' . $this->queryString(array( + 'w' => $w, + 'h' => $h + )) )->then(array($this->parser, 'expectEmpty')); } @@ -1433,13 +1317,9 @@ public function execResize($exec, $w, $h) */ public function execInspect($exec) { + // GET /exec/{exec}/json return $this->browser->get( - $this->uri->expand( - 'exec/{exec}/json', - array( - 'exec' => $exec - ) - ) + 'exec/' . rawurlencode($exec) . '/json' )->then(array($this->parser, 'expectJson')); } @@ -1451,11 +1331,9 @@ public function execInspect($exec) */ public function networkList() { + // GET /networks return $this->browser->get( - $this->uri->expand( - 'networks', - array() - ) + 'networks' )->then(array($this->parser, 'expectJson')); } @@ -1469,13 +1347,9 @@ public function networkList() */ public function networkInspect($network) { + // GET /networks/{network} return $this->browser->get( - $this->uri->expand( - 'networks/{network}', - array( - 'network' => $network - ) - ) + 'networks/' . rawurlencode($network) )->then(array($this->parser, 'expectJson')); } @@ -1489,13 +1363,9 @@ public function networkInspect($network) */ public function networkRemove($network) { + // DELETE /networks/{network} return $this->browser->delete( - $this->uri->expand( - 'networks/{network}', - array( - 'network' => $network - ) - ) + 'networks/' . rawurlencode($network) )->then(array($this->parser, 'expectEmpty')); } @@ -1512,10 +1382,9 @@ public function networkCreate($name, $config = array()) { $config['Name'] = $name; + // POST /networks/create return $this->postJson( - $this->uri->expand( - 'networks/create' - ), + 'networks/create', $config )->then(array($this->parser, 'expectJson')); } @@ -1532,13 +1401,9 @@ public function networkCreate($name, $config = array()) */ public function networkConnect($network, $container, $endpointConfig = array()) { + // POST /networks/{network}/connect return $this->postJson( - $this->uri->expand( - 'networks/{network}/connect', - array( - 'network' => $network - ) - ), + 'networks/' . rawurlencode($network) . '/connect', array( 'Container' => $container, 'EndpointConfig' => $endpointConfig ? $endpointConfig : null @@ -1558,13 +1423,9 @@ public function networkConnect($network, $container, $endpointConfig = array()) */ public function networkDisconnect($network, $container, $force = false) { + // POST /networks/{network}/disconnect return $this->postJson( - $this->uri->expand( - 'networks/{network}/disconnect', - array( - 'network' => $network - ) - ), + 'networks/' . rawurlencode($network) . '/disconnect', array( 'Container' => $container, 'Force' => (bool) $force @@ -1580,11 +1441,9 @@ public function networkDisconnect($network, $container, $force = false) */ public function networkPrune() { + // POST /networks/prune return $this->postJson( - $this->uri->expand( - 'networks/prune', - array() - ), + 'networks/prune', array() )->then(array($this->parser, 'expectJson')); } @@ -1649,4 +1508,25 @@ private function boolArg($value) { return ($value ? 1 : null); } + + /** + * Internal helper function used to build the query string and omit `null` values + * + * Uses `rawurlencode()` because `http_build_query()` would encode `~` and spaces + * differently without `PHP_QUERY_RFC3986`, which requires PHP 5.4+. + * + * @param array $params + * @return string query string with leading `?` or empty string if there is no query + */ + private function queryString(array $params) + { + $query = array(); + foreach ($params as $key => $value) { + if ($value !== null) { + $query[] = $key . '=' . rawurlencode($value); + } + } + + return $query ? '?' . implode('&', $query) : ''; + } } diff --git a/tests/ClientTest.php b/tests/ClientTest.php index 6fb13c2..74f30f3 100644 --- a/tests/ClientTest.php +++ b/tests/ClientTest.php @@ -523,6 +523,18 @@ public function testImageCreate() $this->expectPromiseResolveWith($json, $this->client->imageCreate('busybox')); } + public function testImageCreateFromSrcUrl() + { + $json = array(); + $stream = $this->getMockBuilder('React\Stream\ReadableStreamInterface')->getMock(); + + $this->browser->expects($this->once())->method('requestStreaming')->with('POST', 'images/create?fromSrc=https%3A%2F%2Fexample.com%2F~clue%2Fimage.tar')->willReturn(\React\Promise\resolve($this->createResponseJsonStream($json))); + $this->streamingParser->expects($this->once())->method('parseJsonStream')->willReturn($stream); + $this->streamingParser->expects($this->once())->method('deferredStream')->with($stream)->willReturn(\React\Promise\resolve($json)); + + $this->expectPromiseResolveWith($json, $this->client->imageCreate(null, 'https://example.com/~clue/image.tar')); + } + public function testImageCreateStream() { $stream = $this->getMockBuilder('React\Stream\ReadableStreamInterface')->getMock(); @@ -541,6 +553,14 @@ public function testImageInspect() $this->expectPromiseResolveWith($json, $this->client->imageInspect('123')); } + public function testImageInspectCustomRegistry() + { + $json = array(); + $this->expectRequestFlow('get', 'images/example.com%3A5000%2Fclue%2Fimage%3Alatest/json', $this->createResponseJson($json), 'expectJson'); + + $this->expectPromiseResolveWith($json, $this->client->imageInspect('example.com:5000/clue/image:latest')); + } + public function testImageHistory() { $json = array(); @@ -636,10 +656,9 @@ public function testImageSearch() public function testCommit() { $json = array(); - $config = array(); $this->expectRequestFlow('post', 'commit?container=123&pause=1', $this->createResponseJson($json), 'expectJson'); - $this->expectPromiseResolveWith($json, $this->client->containerCommit('123', $config)); + $this->expectPromiseResolveWith($json, $this->client->containerCommit('123')); } public function testCommitWithoutPause() From 1ad91608ed4030781e26a005bd77f68c700466f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sun, 11 Oct 2026 16:50:00 +0200 Subject: [PATCH 2/2] Remove unneeded `boolArg()` helper --- src/Client.php | 44 ++++++++++++++++---------------------------- 1 file changed, 16 insertions(+), 28 deletions(-) diff --git a/src/Client.php b/src/Client.php index 61d5c92..4897b55 100644 --- a/src/Client.php +++ b/src/Client.php @@ -214,8 +214,8 @@ public function containerList($all = false, $size = false) // GET /containers/json{?all,size} return $this->browser->get( 'containers/json' . $this->queryString(array( - 'all' => $this->boolArg($all), - 'size' => $this->boolArg($size) + 'all' => $all ? 1 : null, + 'size' => $size ? 1 : null )) )->then(array($this->parser, 'expectJson')); } @@ -356,11 +356,11 @@ public function containerLogsStream($container, $follow = false, $stdout = true, // GET /containers/{container}/logs{?follow,stdout,stderr,since,timestamps,tail} $url = 'containers/' . rawurlencode($container) . '/logs' . $this->queryString(array( - 'follow' => $this->boolArg($follow), - 'stdout' => $this->boolArg($stdout), - 'stderr' => $this->boolArg($stderr), + 'follow' => $follow ? 1 : null, + 'stdout' => $stdout ? 1 : null, + 'stderr' => $stderr ? 1 : null, 'since' => ($since === 0) ? null : $since, - 'timestamps' => $this->boolArg($timestamps), + 'timestamps' => $timestamps ? 1 : null, 'tail' => $tail )); @@ -722,10 +722,10 @@ public function containerAttachStream($container, $logs = false, $stream = false // POST /containers/{container}/attach{?logs,stream,stdout,stderr} $url = 'containers/' . rawurlencode($container) . '/attach' . $this->queryString(array( - 'logs' => $this->boolArg($logs), - 'stream' => $this->boolArg($stream), - 'stdout' => $this->boolArg($stdout), - 'stderr' => $this->boolArg($stderr) + 'logs' => $logs ? 1 : null, + 'stream' => $stream ? 1 : null, + 'stdout' => $stdout ? 1 : null, + 'stderr' => $stderr ? 1 : null )); // first inspect container to check TTY setting, then attach with appropriate log parser @@ -772,8 +772,8 @@ public function containerRemove($container, $v = false, $force = false) // DELETE /containers/{container}{?v,force} return $this->browser->delete( 'containers/' . rawurlencode($container) . $this->queryString(array( - 'v' => $this->boolArg($v), - 'force' => $this->boolArg($force) + 'v' => $v ? 1 : null, + 'force' => $force ? 1 : null )) )->then(array($this->parser, 'expectEmpty')); } @@ -862,7 +862,7 @@ public function imageList($all = false) // GET /images/json{?all} return $this->browser->get( 'images/json' . $this->queryString(array( - 'all' => $this->boolArg($all) + 'all' => $all ? 1 : null )) )->then(array($this->parser, 'expectJson')); } @@ -1051,7 +1051,7 @@ public function imageTag($image, $repo, $tag = null, $force = false) 'images/' . rawurlencode($image) . '/tag' . $this->queryString(array( 'repo' => $repo, 'tag' => $tag, - 'force' => $this->boolArg($force) + 'force' => $force ? 1 : null )) )->then(array($this->parser, 'expectEmpty')); } @@ -1070,8 +1070,8 @@ public function imageRemove($image, $force = false, $noprune = false) // DELETE /images/{image}{?force,noprune} return $this->browser->delete( 'images/' . rawurlencode($image) . $this->queryString(array( - 'force' => $this->boolArg($force), - 'noprune' => $this->boolArg($noprune) + 'force' => $force ? 1 : null, + 'noprune' => $noprune ? 1 : null )) )->then(array($this->parser, 'expectEmpty')); } @@ -1497,18 +1497,6 @@ private function authHeaders($registryAuth) return $headers; } - /** - * Internal helper function used to pass boolean true values to endpoints and omit boolean false values - * - * @param boolean $value - * @return int|null returns the integer `1` for boolean true values and a `null` for boolean false values - * @see Browser::resolve() - */ - private function boolArg($value) - { - return ($value ? 1 : null); - } - /** * Internal helper function used to build the query string and omit `null` values *