Completed
Pull Request — master (#148)
by
unknown
06:03
created

ImageManager::build()   B

Complexity

Conditions 5
Paths 14

Size

Total Lines 46
Code Lines 26

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 1
Metric Value
c 1
b 0
f 1
dl 0
loc 46
rs 8.4752
cc 5
eloc 26
nc 14
nop 3
1
<?php
2
3
namespace Docker\Manager;
4
5
use Docker\Exception\ImageNotFoundException;
6
use Docker\Exception\UnexpectedStatusCodeException;
7
use Docker\Image;
8
use GuzzleHttp\Client as HttpClient;
9
use GuzzleHttp\Exception\RequestException;
10
use GuzzleHttp\Message\Response;
11
12
/**
13
 * Docker\Manager\ImageManager
14
 */
15
class ImageManager
16
{
17
    /**
18
     * @var \GuzzleHttp\Client
19
     */
20
    private $client;
21
22
    /**
23
     * @param \GuzzleHttp\Client
24
     */
25
    public function __construct(HttpClient $client)
26
    {
27
        $this->client = $client;
28
    }
29
30
    /**
31
     * Get all images from docker daemon
32
     *
33
     * @param boolean $dangling Filter dangling images
34
     * @param boolean $all      List all images including untagged
35
     *
36
     * @throws \Docker\Exception\UnexpectedStatusCodeException
37
     *
38
     * @return Image[]
39
     */
40
    public function findAll($dangling = false, $all = false)
41
    {
42
        $params = [];
43
44
        if ($all) {
45
            $params['all'] = 1;
46
        }
47
48
        if ($dangling) {
49
            $params['filters'] = json_encode(["dangling" => ["true"]]);
50
        }
51
52
        /** @var Response $response */
53
        $response = $this->client->get('/images/json', [
54
            'query' => $params
55
        ]);
56
57
        if ($response->getStatusCode() !== "200") {
58
            throw UnexpectedStatusCodeException::fromResponse($response);
59
        }
60
61
        $images = $response->json();
62
63
        if (!is_array($images)) {
64
            return [];
65
        }
66
67
        $coll = [];
68
69
        foreach ($images as $data) {
70
            $image = new Image();
71
            $image->setId($data['Id']);
72
            $image->setCreated($data['Created']);
73
74
            foreach ($data['RepoTags'] as $repoTag) {
75
                $tagImage = clone $image;
76
                $tagImage->setRepoTag($repoTag);
77
78
                $coll[] = $tagImage;
79
            }
80
        }
81
82
        return $coll;
83
    }
84
85
    /**
86
     * Get an image from docker daemon
87
     *
88
     * @param string $repository Name of image to get
89
     * @param string $tag        Tag of the image to get (default "latest")
90
     *
91
     * @return Image
92
     */
93
    public function find($repository, $tag = 'latest')
94
    {
95
        $image = new Image($repository, $tag);
96
97
        $data = $this->inspect($image);
98
        $image->setId($data['Id']);
99
        $image->setCreated($data['Created']);
100
101
        return $image;
102
    }
103
104
    /**
105
     * Inspect an image
106
     *
107
     * @param \Docker\Image $image
108
     *
109
     * @throws \Docker\Exception\ImageNotFoundException
110
     * @throws \Docker\Exception\UnexpectedStatusCodeException
111
     * @throws \GuzzleHttp\Exception\RequestException
112
     *
113
     * @return array json data from docker inspect
114
     */
115
    public function inspect(Image $image)
116
    {
117
        try {
118
            # Images need not have a name and tag,(__toString() may return ':')
119
            # so prefer an id hash as the key
120
            if (null != $image->getId()) {
121
              $id = $image->getId();
122
            } else {
123
              $id = $image->__toString();
124
            }
125
126
            $response = $this->client->get(['/images/{id}/json', ['id' => $id]]);
127
128
        } catch (RequestException $e) {
129
            if ($e->hasResponse() && $e->getResponse()->getStatusCode() == "404") {
130
                throw new ImageNotFoundException($id, $e);
131
            }
132
133
            throw $e;
134
        }
135
136
        if ($response->getStatusCode() !== "200") {
137
            throw UnexpectedStatusCodeException::fromResponse($response);
0 ignored issues
show
Compatibility introduced by
$response of type object<GuzzleHttp\Message\ResponseInterface> is not a sub-type of object<GuzzleHttp\Message\Response>. It seems like you assume a concrete implementation of the interface GuzzleHttp\Message\ResponseInterface to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
138
        }
139
140
        return $response->json();
141
    }
142
143
    /**
144
     * Pull an image from registry
145
     *
146
     * @param string   $name     Name of image to pull
147
     * @param string   $tag      Tag of image
148
     * @param callable $callback Callback to retrieve log of pull
149
     *
150
     * @throws \Docker\Exception\UnexpectedStatusCodeException
151
     *
152
     * @return Image
153
     */
154
    public function pull($name, $tag = 'latest', callable $callback = null)
155
    {
156
        if (null === $callback) {
157
            $callback = function () {};
158
        }
159
160
        $response = $this->client->post(['/images/create?fromImage={image}&tag={tag}', ['image' => $name, 'tag' => $tag]], [
161
            'callback' => $callback,
162
            'wait'     => true,
163
        ]);
164
165
        if ($response->getStatusCode() !== "200") {
166
            throw UnexpectedStatusCodeException::fromResponse($response);
0 ignored issues
show
Compatibility introduced by
$response of type object<GuzzleHttp\Message\ResponseInterface> is not a sub-type of object<GuzzleHttp\Message\Response>. It seems like you assume a concrete implementation of the interface GuzzleHttp\Message\ResponseInterface to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
167
        }
168
169
        $image = new Image($name, $tag);
170
        $data = $this->inspect($image);
171
172
        if (!$image->getId()) {
173
            $image->setId($data['Id']);
174
            $image->setCreated($data['Created']);
175
        }
176
177
        return $image;
178
    }
179
180
    /**
181
     * Push an image.
182
     *
183
     * @param string   $name
184
     * @param string   $tag
185
     * @param string   $registryAuth
186
     * @param callable $callback
187
     *
188
     * @throws \Docker\Exception\UnexpectedStatusCodeException
189
     *
190
     * @return Image
191
     */
192
    public function push($name, $tag, $registryAuth, callable $callback = null)
193
    {
194
        if (null === $callback) {
195
            $callback = function () {};
196
        }
197
198
        $response = $this->client->post(['/images/{image}/push', ['image' => $name, 'tag' => $tag]], [
199
            'headers' => [
200
                'X-Registry-Auth' => $registryAuth
201
            ],
202
            'callback' => $callback,
203
            'wait'     => true,
204
        ]);
205
206
        if ($response->getStatusCode() !== "200") {
207
            throw UnexpectedStatusCodeException::fromResponse($response);
0 ignored issues
show
Compatibility introduced by
$response of type object<GuzzleHttp\Message\ResponseInterface> is not a sub-type of object<GuzzleHttp\Message\Response>. It seems like you assume a concrete implementation of the interface GuzzleHttp\Message\ResponseInterface to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
208
        }
209
210
        return new Image($name, $tag);
211
    }
212
213
    /**
214
     * Build docker image (Docker API v1.20)
215
     *
216
     * POST /build request returns empty body with different headers so it's not needed to build something
217
     * like UnexpectedStatusCodeException. We can get response success or fail only.
218
     *
219
     * @param array $options
220
     *   dockerfile - Path within the build context to the Dockerfile. This is ignored if remote is specified and points to an individual filename.
221
     *   t – A repository name (and optionally a tag) to apply to the resulting image in case of success.
222
     *   remote – A Git repository URI or HTTP/HTTPS URI build source. If the URI specifies a filename, the file’s contents are placed into a file called Dockerfile.
223
     *   q – Suppress verbose build output.
224
     *   nocache – Do not use the cache when building the image.
225
     *   pull - Attempt to pull the image even if an older image exists locally.
226
     *   rm - Remove intermediate containers after a successful build (default behavior).
227
     *   forcerm - Always remove intermediate containers (includes rm).
228
     *   memory - Set memory limit for build.
229
     *   memswap - Total memory (memory + swap), -1 to disable swap.
230
     *   cpushares - CPU shares (relative weight).
231
     *   cpusetcpus - CPUs in which to allow execution (e.g., 0-3, 0,1).
232
     *   cpuperiod - The length of a CPU period in microseconds.
233
     *   cpuquota - Microseconds of CPU time that the container can get in a CPU period
234
     *
235
     * @param $dockerfileString - Dockerfile contents
236
     * @param callable|null $callback
237
     * @return bool
238
     * @throws \GuzzleHttp\Exception\RequestException
239
     */
240
    public function build($dockerfileString, callable $callback = null, array $options = [])
241
    {
242
        if (null === $callback) {
243
            $callback = function () {};
244
        }
245
246
        // Create archive with Dockerfile content
247
        $phar = new \PharData(sys_get_temp_dir() . DIRECTORY_SEPARATOR . uniqid('docker_php_dockerfile_tmp_', true) . '.tar');
248
        $phar['Dockerfile'] = $dockerfileString;
249
        $dockerfileCompressed = $phar->compress(\Phar::GZ);
250
        $dockerfileResource = fopen($dockerfileCompressed->getPath(), 'r');
251
252
        // remove file from cache
253
        unlink($phar->getPath());
254
        unset($phar);
255
256
        $caughtException = null;
257
        try {
258
            $response = $this->client->post('/build?' . http_build_query($options), [
259
                'headers' => [
260
                    'Content-Type' => 'application/tar',
261
                ],
262
                'body' => $dockerfileResource,
263
                'callback' => $callback,
264
                'wait' => true,
265
            ]);
266
267
            if ($response->getStatusCode() !== '200') {
268
                return false;
269
            }
270
        } catch(RequestException $e) {
271
            /** @var RequestException $caughtException */
272
            $caughtException = $e;
273
        }
274
275
        // remove second archive from cache
276
        unlink($dockerfileCompressed->getPath());
277
        unset($dockerfileCompressed);
278
279
        // this is try-finally replacement for current version php54
280
        if ($caughtException) {
281
            throw $caughtException;
282
        }
283
284
        return true;
285
    }
286
287
    /**
288
     * Remove an image from docker daemon
289
     *
290
     * @param Image   $image   Image to remove
291
     * @param boolean $force   Force removal of image (default false)
292
     * @param boolean $noprune Do not remove parent images (default false)
293
     *
294
     * @throws \Docker\Exception\UnexpectedStatusCodeException
295
     *
296
     * @return ImageManager
297
     */
298 View Code Duplication
    public function remove(Image $image, $force = false, $noprune = false)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
299
    {
300
        $response = $this->client->delete(['/images/{image}?force={force}&noprune={noprune}', [
301
            'image'   => $image->__toString(),
302
            'force'   => $force,
303
            'noprune' => $noprune,
304
            'wait'    => true
305
        ]]);
306
307
        if ($response->getStatusCode() !== "200") {
308
            throw UnexpectedStatusCodeException::fromResponse($response);
0 ignored issues
show
Compatibility introduced by
$response of type object<GuzzleHttp\Message\ResponseInterface> is not a sub-type of object<GuzzleHttp\Message\Response>. It seems like you assume a concrete implementation of the interface GuzzleHttp\Message\ResponseInterface to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
309
        }
310
311
        return $this;
312
    }
313
314
    /**
315
     * Remove multiple images from docker daemon
316
     *
317
     * @param Image[]|array $images  Images to remove
318
     * @param boolean       $force   Force removal of image (default false)
319
     * @param boolean       $noprune Do not remove parent images (default false)
320
     *
321
     * @throws \Docker\Exception\UnexpectedStatusCodeException
322
     *
323
     * @return ImageManager
324
     */
325
    public function removeImages(array $images, $force = false, $noprune = false)
326
    {
327
        foreach ($images as $image) {
328
            if (!$image instanceof Image) {
329
                $imageId = $image;
330
331
                $image = new Image();
332
                $image->setId($imageId);
333
            }
334
335
            $this->remove($image, $force, $noprune);
336
        }
337
338
        return $this;
339
    }
340
341
    /**
342
     * Search for an image on Docker Hub.
343
     *
344
     * @param string $term term to search
345
     *
346
     * @throws \Docker\Exception\UnexpectedStatusCodeException
347
     *
348
     * @return array
349
     */
350
    public function search($term)
351
    {
352
        $response = $this->client->get(
353
            [
354
                '/images/search?term={term}',
355
                [
356
                    'term' => $term,
357
                ]
358
            ]
359
        );
360
361
        if ($response->getStatusCode() !== "200") {
362
            throw UnexpectedStatusCodeException::fromResponse($response);
0 ignored issues
show
Compatibility introduced by
$response of type object<GuzzleHttp\Message\ResponseInterface> is not a sub-type of object<GuzzleHttp\Message\Response>. It seems like you assume a concrete implementation of the interface GuzzleHttp\Message\ResponseInterface to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
363
        }
364
365
        return $response->json();
366
    }
367
368
    /**
369
     * Tag an image
370
     *
371
     * @param Image $image image to tag
372
     * @param $repository Repository name to use
373
     * @param string $tag Tag to use
374
     * @param bool $force Force to set tag even if an image with the same name already exists ?
375
     *
376
     * @throws \Docker\Exception\UnexpectedStatusCodeException
377
     *
378
     * @return ImageManager
379
     */
380
    public function tag(Image $image, $repository, $tag = 'latest', $force = false)
381
    {
382
        $response = $this->client->post([
383
            '/images/{name}/tag?repo={repository}&tag={tag}&force={force}', [
384
                'name' => $image->getId(),
385
                'repository' => $repository,
386
                'tag' => $tag,
387
                'force' => intval($force)
388
            ]
389
        ]);
390
391
        if ($response->getStatusCode() !== "201") {
392
            throw UnexpectedStatusCodeException::fromResponse($response);
0 ignored issues
show
Compatibility introduced by
$response of type object<GuzzleHttp\Message\ResponseInterface> is not a sub-type of object<GuzzleHttp\Message\Response>. It seems like you assume a concrete implementation of the interface GuzzleHttp\Message\ResponseInterface to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
393
        }
394
395
        $image->setRepository($repository);
396
        $image->setTag($tag);
397
398
        return $this;
399
    }
400
401
    /**
402
     * Get history of an image
403
     *
404
     * @param Image $image
405
     *
406
     * @throws \Docker\Exception\UnexpectedStatusCodeException
407
     *
408
     * @return array
409
     */
410 View Code Duplication
    public function history(Image $image)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
411
    {
412
        $response = $this->client->get(['/images/{name}/history', [
413
            'name' => $image->__toString()
414
        ]]);
415
416
        if ($response->getStatusCode() !== "200") {
417
            throw UnexpectedStatusCodeException::fromResponse($response);
0 ignored issues
show
Compatibility introduced by
$response of type object<GuzzleHttp\Message\ResponseInterface> is not a sub-type of object<GuzzleHttp\Message\Response>. It seems like you assume a concrete implementation of the interface GuzzleHttp\Message\ResponseInterface to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
418
        }
419
420
        return $response->json();
421
    }
422
}
423