1import logging
2import os
3
4import six
5
6from .. import auth, errors, utils
7from ..constants import DEFAULT_DATA_CHUNK_SIZE
8
9log = logging.getLogger(__name__)
10
11
12class ImageApiMixin(object):
13
14    @utils.check_resource('image')
15    def get_image(self, image, chunk_size=DEFAULT_DATA_CHUNK_SIZE):
16        """
17        Get a tarball of an image. Similar to the ``docker save`` command.
18
19        Args:
20            image (str): Image name to get
21            chunk_size (int): The number of bytes returned by each iteration
22                of the generator. If ``None``, data will be streamed as it is
23                received. Default: 2 MB
24
25        Returns:
26            (generator): A stream of raw archive data.
27
28        Raises:
29            :py:class:`docker.errors.APIError`
30                If the server returns an error.
31
32        Example:
33
34            >>> image = cli.get_image("busybox:latest")
35            >>> f = open('/tmp/busybox-latest.tar', 'wb')
36            >>> for chunk in image:
37            >>>   f.write(chunk)
38            >>> f.close()
39        """
40        res = self._get(self._url("/images/{0}/get", image), stream=True)
41        return self._stream_raw_result(res, chunk_size, False)
42
43    @utils.check_resource('image')
44    def history(self, image):
45        """
46        Show the history of an image.
47
48        Args:
49            image (str): The image to show history for
50
51        Returns:
52            (str): The history of the image
53
54        Raises:
55            :py:class:`docker.errors.APIError`
56                If the server returns an error.
57        """
58        res = self._get(self._url("/images/{0}/history", image))
59        return self._result(res, True)
60
61    def images(self, name=None, quiet=False, all=False, filters=None):
62        """
63        List images. Similar to the ``docker images`` command.
64
65        Args:
66            name (str): Only show images belonging to the repository ``name``
67            quiet (bool): Only return numeric IDs as a list.
68            all (bool): Show intermediate image layers. By default, these are
69                filtered out.
70            filters (dict): Filters to be processed on the image list.
71                Available filters:
72                - ``dangling`` (bool)
73                - `label` (str|list): format either ``"key"``, ``"key=value"``
74                    or a list of such.
75
76        Returns:
77            (dict or list): A list if ``quiet=True``, otherwise a dict.
78
79        Raises:
80            :py:class:`docker.errors.APIError`
81                If the server returns an error.
82        """
83        params = {
84            'filter': name,
85            'only_ids': 1 if quiet else 0,
86            'all': 1 if all else 0,
87        }
88        if filters:
89            params['filters'] = utils.convert_filters(filters)
90        res = self._result(self._get(self._url("/images/json"), params=params),
91                           True)
92        if quiet:
93            return [x['Id'] for x in res]
94        return res
95
96    def import_image(self, src=None, repository=None, tag=None, image=None,
97                     changes=None, stream_src=False):
98        """
99        Import an image. Similar to the ``docker import`` command.
100
101        If ``src`` is a string or unicode string, it will first be treated as a
102        path to a tarball on the local system. If there is an error reading
103        from that file, ``src`` will be treated as a URL instead to fetch the
104        image from. You can also pass an open file handle as ``src``, in which
105        case the data will be read from that file.
106
107        If ``src`` is unset but ``image`` is set, the ``image`` parameter will
108        be taken as the name of an existing image to import from.
109
110        Args:
111            src (str or file): Path to tarfile, URL, or file-like object
112            repository (str): The repository to create
113            tag (str): The tag to apply
114            image (str): Use another image like the ``FROM`` Dockerfile
115                parameter
116        """
117        if not (src or image):
118            raise errors.DockerException(
119                'Must specify src or image to import from'
120            )
121        u = self._url('/images/create')
122
123        params = _import_image_params(
124            repository, tag, image,
125            src=(src if isinstance(src, six.string_types) else None),
126            changes=changes
127        )
128        headers = {'Content-Type': 'application/tar'}
129
130        if image or params.get('fromSrc') != '-':  # from image or URL
131            return self._result(
132                self._post(u, data=None, params=params)
133            )
134        elif isinstance(src, six.string_types):  # from file path
135            with open(src, 'rb') as f:
136                return self._result(
137                    self._post(
138                        u, data=f, params=params, headers=headers, timeout=None
139                    )
140                )
141        else:  # from raw data
142            if stream_src:
143                headers['Transfer-Encoding'] = 'chunked'
144            return self._result(
145                self._post(u, data=src, params=params, headers=headers)
146            )
147
148    def import_image_from_data(self, data, repository=None, tag=None,
149                               changes=None):
150        """
151        Like :py:meth:`~docker.api.image.ImageApiMixin.import_image`, but
152        allows importing in-memory bytes data.
153
154        Args:
155            data (bytes collection): Bytes collection containing valid tar data
156            repository (str): The repository to create
157            tag (str): The tag to apply
158        """
159
160        u = self._url('/images/create')
161        params = _import_image_params(
162            repository, tag, src='-', changes=changes
163        )
164        headers = {'Content-Type': 'application/tar'}
165        return self._result(
166            self._post(
167                u, data=data, params=params, headers=headers, timeout=None
168            )
169        )
170
171    def import_image_from_file(self, filename, repository=None, tag=None,
172                               changes=None):
173        """
174        Like :py:meth:`~docker.api.image.ImageApiMixin.import_image`, but only
175        supports importing from a tar file on disk.
176
177        Args:
178            filename (str): Full path to a tar file.
179            repository (str): The repository to create
180            tag (str): The tag to apply
181
182        Raises:
183            IOError: File does not exist.
184        """
185
186        return self.import_image(
187            src=filename, repository=repository, tag=tag, changes=changes
188        )
189
190    def import_image_from_stream(self, stream, repository=None, tag=None,
191                                 changes=None):
192        return self.import_image(
193            src=stream, stream_src=True, repository=repository, tag=tag,
194            changes=changes
195        )
196
197    def import_image_from_url(self, url, repository=None, tag=None,
198                              changes=None):
199        """
200        Like :py:meth:`~docker.api.image.ImageApiMixin.import_image`, but only
201        supports importing from a URL.
202
203        Args:
204            url (str): A URL pointing to a tar file.
205            repository (str): The repository to create
206            tag (str): The tag to apply
207        """
208        return self.import_image(
209            src=url, repository=repository, tag=tag, changes=changes
210        )
211
212    def import_image_from_image(self, image, repository=None, tag=None,
213                                changes=None):
214        """
215        Like :py:meth:`~docker.api.image.ImageApiMixin.import_image`, but only
216        supports importing from another image, like the ``FROM`` Dockerfile
217        parameter.
218
219        Args:
220            image (str): Image name to import from
221            repository (str): The repository to create
222            tag (str): The tag to apply
223        """
224        return self.import_image(
225            image=image, repository=repository, tag=tag, changes=changes
226        )
227
228    @utils.check_resource('image')
229    def inspect_image(self, image):
230        """
231        Get detailed information about an image. Similar to the ``docker
232        inspect`` command, but only for images.
233
234        Args:
235            image (str): The image to inspect
236
237        Returns:
238            (dict): Similar to the output of ``docker inspect``, but as a
239        single dict
240
241        Raises:
242            :py:class:`docker.errors.APIError`
243                If the server returns an error.
244        """
245        return self._result(
246            self._get(self._url("/images/{0}/json", image)), True
247        )
248
249    @utils.minimum_version('1.30')
250    @utils.check_resource('image')
251    def inspect_distribution(self, image, auth_config=None):
252        """
253        Get image digest and platform information by contacting the registry.
254
255        Args:
256            image (str): The image name to inspect
257            auth_config (dict): Override the credentials that are found in the
258                config for this request.  ``auth_config`` should contain the
259                ``username`` and ``password`` keys to be valid.
260
261        Returns:
262            (dict): A dict containing distribution data
263
264        Raises:
265            :py:class:`docker.errors.APIError`
266                If the server returns an error.
267        """
268        registry, _ = auth.resolve_repository_name(image)
269
270        headers = {}
271        if auth_config is None:
272            header = auth.get_config_header(self, registry)
273            if header:
274                headers['X-Registry-Auth'] = header
275        else:
276            log.debug('Sending supplied auth config')
277            headers['X-Registry-Auth'] = auth.encode_header(auth_config)
278
279        url = self._url("/distribution/{0}/json", image)
280
281        return self._result(
282            self._get(url, headers=headers), True
283        )
284
285    def load_image(self, data, quiet=None):
286        """
287        Load an image that was previously saved using
288        :py:meth:`~docker.api.image.ImageApiMixin.get_image` (or ``docker
289        save``). Similar to ``docker load``.
290
291        Args:
292            data (binary): Image data to be loaded.
293            quiet (boolean): Suppress progress details in response.
294
295        Returns:
296            (generator): Progress output as JSON objects. Only available for
297                         API version >= 1.23
298
299        Raises:
300            :py:class:`docker.errors.APIError`
301                If the server returns an error.
302        """
303        params = {}
304
305        if quiet is not None:
306            if utils.version_lt(self._version, '1.23'):
307                raise errors.InvalidVersion(
308                    'quiet is not supported in API version < 1.23'
309                )
310            params['quiet'] = quiet
311
312        res = self._post(
313            self._url("/images/load"), data=data, params=params, stream=True
314        )
315        if utils.version_gte(self._version, '1.23'):
316            return self._stream_helper(res, decode=True)
317
318        self._raise_for_status(res)
319
320    @utils.minimum_version('1.25')
321    def prune_images(self, filters=None):
322        """
323        Delete unused images
324
325        Args:
326            filters (dict): Filters to process on the prune list.
327                Available filters:
328                - dangling (bool):  When set to true (or 1), prune only
329                unused and untagged images.
330
331        Returns:
332            (dict): A dict containing a list of deleted image IDs and
333                the amount of disk space reclaimed in bytes.
334
335        Raises:
336            :py:class:`docker.errors.APIError`
337                If the server returns an error.
338        """
339        url = self._url("/images/prune")
340        params = {}
341        if filters is not None:
342            params['filters'] = utils.convert_filters(filters)
343        return self._result(self._post(url, params=params), True)
344
345    def pull(self, repository, tag=None, stream=False, auth_config=None,
346             decode=False, platform=None):
347        """
348        Pulls an image. Similar to the ``docker pull`` command.
349
350        Args:
351            repository (str): The repository to pull
352            tag (str): The tag to pull
353            stream (bool): Stream the output as a generator. Make sure to
354                consume the generator, otherwise pull might get cancelled.
355            auth_config (dict): Override the credentials that are found in the
356                config for this request.  ``auth_config`` should contain the
357                ``username`` and ``password`` keys to be valid.
358            decode (bool): Decode the JSON data from the server into dicts.
359                Only applies with ``stream=True``
360            platform (str): Platform in the format ``os[/arch[/variant]]``
361
362        Returns:
363            (generator or str): The output
364
365        Raises:
366            :py:class:`docker.errors.APIError`
367                If the server returns an error.
368
369        Example:
370
371            >>> for line in cli.pull('busybox', stream=True, decode=True):
372            ...     print(json.dumps(line, indent=4))
373            {
374                "status": "Pulling image (latest) from busybox",
375                "progressDetail": {},
376                "id": "e72ac664f4f0"
377            }
378            {
379                "status": "Pulling image (latest) from busybox, endpoint: ...",
380                "progressDetail": {},
381                "id": "e72ac664f4f0"
382            }
383
384        """
385        if not tag:
386            repository, tag = utils.parse_repository_tag(repository)
387        registry, repo_name = auth.resolve_repository_name(repository)
388
389        params = {
390            'tag': tag,
391            'fromImage': repository
392        }
393        headers = {}
394
395        if auth_config is None:
396            header = auth.get_config_header(self, registry)
397            if header:
398                headers['X-Registry-Auth'] = header
399        else:
400            log.debug('Sending supplied auth config')
401            headers['X-Registry-Auth'] = auth.encode_header(auth_config)
402
403        if platform is not None:
404            if utils.version_lt(self._version, '1.32'):
405                raise errors.InvalidVersion(
406                    'platform was only introduced in API version 1.32'
407                )
408            params['platform'] = platform
409
410        response = self._post(
411            self._url('/images/create'), params=params, headers=headers,
412            stream=stream, timeout=None
413        )
414
415        self._raise_for_status(response)
416
417        if stream:
418            return self._stream_helper(response, decode=decode)
419
420        return self._result(response)
421
422    def push(self, repository, tag=None, stream=False, auth_config=None,
423             decode=False):
424        """
425        Push an image or a repository to the registry. Similar to the ``docker
426        push`` command.
427
428        Args:
429            repository (str): The repository to push to
430            tag (str): An optional tag to push
431            stream (bool): Stream the output as a blocking generator
432            auth_config (dict): Override the credentials that are found in the
433                config for this request.  ``auth_config`` should contain the
434                ``username`` and ``password`` keys to be valid.
435            decode (bool): Decode the JSON data from the server into dicts.
436                Only applies with ``stream=True``
437
438        Returns:
439            (generator or str): The output from the server.
440
441        Raises:
442            :py:class:`docker.errors.APIError`
443                If the server returns an error.
444
445        Example:
446            >>> for line in cli.push('yourname/app', stream=True, decode=True):
447            ...   print(line)
448            {'status': 'Pushing repository yourname/app (1 tags)'}
449            {'status': 'Pushing','progressDetail': {}, 'id': '511136ea3c5a'}
450            {'status': 'Image already pushed, skipping', 'progressDetail':{},
451             'id': '511136ea3c5a'}
452            ...
453
454        """
455        if not tag:
456            repository, tag = utils.parse_repository_tag(repository)
457        registry, repo_name = auth.resolve_repository_name(repository)
458        u = self._url("/images/{0}/push", repository)
459        params = {
460            'tag': tag
461        }
462        headers = {}
463
464        if auth_config is None:
465            header = auth.get_config_header(self, registry)
466            if header:
467                headers['X-Registry-Auth'] = header
468        else:
469            log.debug('Sending supplied auth config')
470            headers['X-Registry-Auth'] = auth.encode_header(auth_config)
471
472        response = self._post_json(
473            u, None, headers=headers, stream=stream, params=params
474        )
475
476        self._raise_for_status(response)
477
478        if stream:
479            return self._stream_helper(response, decode=decode)
480
481        return self._result(response)
482
483    @utils.check_resource('image')
484    def remove_image(self, image, force=False, noprune=False):
485        """
486        Remove an image. Similar to the ``docker rmi`` command.
487
488        Args:
489            image (str): The image to remove
490            force (bool): Force removal of the image
491            noprune (bool): Do not delete untagged parents
492        """
493        params = {'force': force, 'noprune': noprune}
494        res = self._delete(self._url("/images/{0}", image), params=params)
495        return self._result(res, True)
496
497    def search(self, term):
498        """
499        Search for images on Docker Hub. Similar to the ``docker search``
500        command.
501
502        Args:
503            term (str): A term to search for.
504
505        Returns:
506            (list of dicts): The response of the search.
507
508        Raises:
509            :py:class:`docker.errors.APIError`
510                If the server returns an error.
511        """
512        return self._result(
513            self._get(self._url("/images/search"), params={'term': term}),
514            True
515        )
516
517    @utils.check_resource('image')
518    def tag(self, image, repository, tag=None, force=False):
519        """
520        Tag an image into a repository. Similar to the ``docker tag`` command.
521
522        Args:
523            image (str): The image to tag
524            repository (str): The repository to set for the tag
525            tag (str): The tag name
526            force (bool): Force
527
528        Returns:
529            (bool): ``True`` if successful
530
531        Raises:
532            :py:class:`docker.errors.APIError`
533                If the server returns an error.
534
535        Example:
536
537            >>> client.tag('ubuntu', 'localhost:5000/ubuntu', 'latest',
538                           force=True)
539        """
540        params = {
541            'tag': tag,
542            'repo': repository,
543            'force': 1 if force else 0
544        }
545        url = self._url("/images/{0}/tag", image)
546        res = self._post(url, params=params)
547        self._raise_for_status(res)
548        return res.status_code == 201
549
550
551def is_file(src):
552    try:
553        return (
554            isinstance(src, six.string_types) and
555            os.path.isfile(src)
556        )
557    except TypeError:  # a data string will make isfile() raise a TypeError
558        return False
559
560
561def _import_image_params(repo, tag, image=None, src=None,
562                         changes=None):
563    params = {
564        'repo': repo,
565        'tag': tag,
566    }
567    if image:
568        params['fromImage'] = image
569    elif src and not is_file(src):
570        params['fromSrc'] = src
571    else:
572        params['fromSrc'] = '-'
573
574    if changes:
575        params['changes'] = changes
576
577    return params
578