Pewarta Proxy

📋 Server Request & Proxy Logs

Loading active proxy settings...
Developed by Khairul Adha (adhexa.id)
Mengambil log server...

⚙️ Pengaturan Proxy Target

Skip to content

Commit 0c271fa

Browse files
committed
feat(build): accept several tags in imageBuild()
Docker applies each repeated `t` query parameter as a tag, but the generated endpoint only accepted a single string. Accept a string or a list of strings for `t` and send one `t=` pair per tag. Keyed arrays are reindexed so they don't produce bracketed keys. Declare the wider type in Docker::imageBuild()'s parameter shape and document it in the build recipe. Cover the query string, an invalid tag type and a real build against the daemon. Closes #287
1 parent 1cbbce1 commit 0c271fa

6 files changed

Lines changed: 110 additions & 0 deletions

File tree

‎docs/cookbook/build-image.mdx‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,10 @@ $build->wait();
7979
Do not pre-URL-encode the JSON options. Declare `ARG APP_MODE` in the Dockerfile
8080
if the build uses it. Build arguments are not a safe channel for secrets.
8181

82+
To apply several tags, pass a list to `t`, for example
83+
`'t' => ['docker-php-example:latest', 'docker-php-example:1.0']`. Docker PHP
84+
sends one `t` parameter per tag. Lists require client `3.3.0` or later.
85+
8286
A streamed context is consumed by the first request. The original tar process
8387
is not rewindable. Keep the context alive until the request has
8488
finished, and do not assume a process-backed stream has a known byte length.

‎src/Docker.php‎

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,34 @@ public function execStartInteractive(string $id, ?ExecIdStartPostBody $requestBo
131131
/**
132132
* {@inheritdoc}
133133
*
134+
* @param array{
135+
* "dockerfile"?: string,
136+
* "t"?: string|list<string>,
137+
* "extrahosts"?: string,
138+
* "remote"?: string,
139+
* "q"?: bool,
140+
* "nocache"?: bool,
141+
* "cachefrom"?: string,
142+
* "pull"?: string,
143+
* "rm"?: bool,
144+
* "forcerm"?: bool,
145+
* "memory"?: int,
146+
* "memswap"?: int,
147+
* "cpushares"?: int,
148+
* "cpusetcpus"?: string,
149+
* "cpuperiod"?: int,
150+
* "cpuquota"?: int,
151+
* "buildargs"?: string,
152+
* "shmsize"?: int,
153+
* "squash"?: bool,
154+
* "labels"?: string,
155+
* "networkmode"?: string,
156+
* "platform"?: string,
157+
* "target"?: string,
158+
* "outputs"?: string,
159+
* "version"?: string,
160+
* } $queryParameters
161+
*
134162
* @return ($fetch is 'object' ? BuildStream|null : ResponseInterface)
135163
*/
136164
public function imageBuild($requestBody = null, array $queryParameters = [], array $headerParameters = [], string $fetch = self::FETCH_OBJECT)

‎src/Endpoint/ImageBuild.php‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
use Docker\Stream\TarStream;
1010
use Nyholm\Psr7\Stream;
1111
use Psr\Http\Message\ResponseInterface;
12+
use Symfony\Component\OptionsResolver\Options;
13+
use Symfony\Component\OptionsResolver\OptionsResolver;
1214
use Symfony\Component\Serializer\SerializerInterface;
1315

1416
class ImageBuild extends BaseEndpoint
@@ -24,6 +26,21 @@ public function getBody(SerializerInterface $serializer, $streamFactory = null):
2426
return [['Content-Type' => ['application/octet-stream']], $body];
2527
}
2628

29+
protected function getQueryOptionsResolver(): OptionsResolver
30+
{
31+
$optionsResolver = parent::getQueryOptionsResolver();
32+
// Docker applies each repeated `t` parameter as a separate tag.
33+
$optionsResolver->setAllowedTypes('t', ['string', 'string[]']);
34+
$optionsResolver->setNormalizer('t', static fn (Options $options, $value) => \is_array($value) ? array_values($value) : $value);
35+
36+
return $optionsResolver;
37+
}
38+
39+
protected function getQueryStyles(): array
40+
{
41+
return ['t' => ['style' => 'form', 'explode' => true]] + parent::getQueryStyles();
42+
}
43+
2744
protected function transformResponseBody(ResponseInterface $response, SerializerInterface $serializer, ?string $contentType = null)
2845
{
2946
if (200 === $response->getStatusCode()) {

‎tests/Endpoint/ImageBuildTest.php‎

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Docker\Tests\Endpoint;
6+
7+
use Docker\Endpoint\ImageBuild;
8+
use PHPUnit\Framework\Attributes\DataProvider;
9+
use PHPUnit\Framework\TestCase;
10+
use Symfony\Component\OptionsResolver\Exception\InvalidOptionsException;
11+
12+
class ImageBuildTest extends TestCase
13+
{
14+
public static function tagProvider(): iterable
15+
{
16+
yield 'single tag' => ['app:latest', ['t=app%3Alatest']];
17+
yield 'several tags' => [['app:latest', 'app:1.0'], ['t=app%3Alatest', 't=app%3A1.0']];
18+
yield 'keyed tags' => [['a' => 'app:latest', 'b' => 'app:1.0'], ['t=app%3Alatest', 't=app%3A1.0']];
19+
yield 'no tags' => [[], []];
20+
}
21+
22+
#[DataProvider('tagProvider')]
23+
public function testRepeatsTagParameter(string|array $tags, array $expected): void
24+
{
25+
$query = (new ImageBuild(null, ['t' => $tags]))->getQueryString();
26+
$parameters = explode('&', $query);
27+
28+
$this->assertSame($expected, array_values(array_filter($parameters, static fn (string $pair): bool => str_starts_with($pair, 't='))));
29+
$this->assertContains('dockerfile=Dockerfile', $parameters);
30+
}
31+
32+
public function testRejectsNonStringTags(): void
33+
{
34+
$this->expectException(InvalidOptionsException::class);
35+
36+
(new ImageBuild(null, ['t' => ['app:latest', 1]]))->getQueryString();
37+
}
38+
}

‎tests/Resource/ImageResourceTest.php‎

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,28 @@ public function testBuild(): void
4040
$this->assertStringContainsString('Successfully', $lastMessage);
4141
}
4242

43+
public function testBuildAppliesSeveralTags(): void
44+
{
45+
$contextBuilder = new ContextBuilder();
46+
$contextBuilder->from('busybox:latest');
47+
$contextBuilder->add('/test', 'test file content');
48+
$context = $contextBuilder->getContext();
49+
$tags = ['docker-php-test-tags:one', 'docker-php-test-tags:two'];
50+
51+
$buildStream = $this->getManager()->imageBuild($context->read(), ['t' => $tags]);
52+
$buildStream->wait();
53+
54+
try {
55+
$repoTags = $this->getManager()->imageInspect($tags[0])->getRepoTags();
56+
sort($repoTags);
57+
$this->assertSame($tags, $repoTags);
58+
} finally {
59+
foreach ($tags as $tag) {
60+
$this->getManager()->imageDelete($tag);
61+
}
62+
}
63+
}
64+
4365
public function testCreate(): void
4466
{
4567
$createImageStream = $this->getManager()->imageCreate('', [

‎tests/StaticAnalysis/DockerReturnTypes.php‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ function dockerReturnTypes(Docker $docker): void
1616
assertType('Docker\Stream\DockerRawStream|null', $docker->containerLogs('id'));
1717
assertType('Docker\Stream\DockerRawStream|null', $docker->execStart('id'));
1818
assertType('Docker\Stream\BuildStream|null', $docker->imageBuild());
19+
assertType('Docker\Stream\BuildStream|null', $docker->imageBuild(null, ['t' => ['app:latest', 'app:1.0']]));
1920
assertType('Docker\Stream\CreateImageStream|null', $docker->imageCreate());
2021
assertType('Docker\Stream\PushStream|null', $docker->imagePush('name'));
2122
assertType('Docker\Stream\EventStream|null', $docker->systemEvents());

0 commit comments

Comments
 (0)