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