1#!/usr/bin/python 2# 3# Copyright 2016 Red Hat | Ansible 4# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) 5 6from __future__ import absolute_import, division, print_function 7__metaclass__ = type 8 9 10ANSIBLE_METADATA = {'metadata_version': '1.1', 11 'status': ['preview'], 12 'supported_by': 'community'} 13 14 15DOCUMENTATION = ''' 16--- 17module: docker_image 18 19short_description: Manage docker images. 20 21version_added: "1.5" 22 23description: 24 - Build, load or pull an image, making the image available for creating containers. Also supports tagging an 25 image into a repository and archiving an image to a .tar file. 26 - Since Ansible 2.8, it is recommended to explicitly specify the image's source (I(source) can be C(build), 27 C(load), C(pull) or C(local)). This will be required from Ansible 2.12 on. 28 29options: 30 source: 31 description: 32 - "Determines where the module will try to retrieve the image from." 33 - "Use C(build) to build the image from a C(Dockerfile). I(build.path) must 34 be specified when this value is used." 35 - "Use C(load) to load the image from a C(.tar) file. I(load_path) must 36 be specified when this value is used." 37 - "Use C(pull) to pull the image from a registry." 38 - "Use C(local) to make sure that the image is already available on the local 39 docker daemon, i.e. do not try to build, pull or load the image." 40 - "Before Ansible 2.12, the value of this option will be auto-detected 41 to be backwards compatible, but a warning will be issued if it is not 42 explicitly specified. From Ansible 2.12 on, auto-detection will be disabled 43 and this option will be made mandatory." 44 type: str 45 choices: 46 - build 47 - load 48 - pull 49 - local 50 version_added: "2.8" 51 build: 52 description: 53 - "Specifies options used for building images." 54 type: dict 55 suboptions: 56 cache_from: 57 description: 58 - List of image names to consider as cache source. 59 type: list 60 elements: str 61 dockerfile: 62 description: 63 - Use with state C(present) and source C(build) to provide an alternate name for the Dockerfile to use when building an image. 64 - This can also include a relative path (relative to I(path)). 65 type: str 66 http_timeout: 67 description: 68 - Timeout for HTTP requests during the image build operation. Provide a positive integer value for the number of 69 seconds. 70 type: int 71 path: 72 description: 73 - Use with state 'present' to build an image. Will be the path to a directory containing the context and 74 Dockerfile for building an image. 75 type: path 76 required: yes 77 pull: 78 description: 79 - When building an image downloads any updates to the FROM image in Dockerfile. 80 - The default is currently C(yes). This will change to C(no) in Ansible 2.12. 81 type: bool 82 rm: 83 description: 84 - Remove intermediate containers after build. 85 type: bool 86 default: yes 87 network: 88 description: 89 - The network to use for C(RUN) build instructions. 90 type: str 91 nocache: 92 description: 93 - Do not use cache when building an image. 94 type: bool 95 default: no 96 etc_hosts: 97 description: 98 - Extra hosts to add to C(/etc/hosts) in building containers, as a mapping of hostname to IP address. 99 type: dict 100 version_added: "2.9" 101 args: 102 description: 103 - Provide a dictionary of C(key:value) build arguments that map to Dockerfile ARG directive. 104 - Docker expects the value to be a string. For convenience any non-string values will be converted to strings. 105 - Requires Docker API >= 1.21. 106 type: dict 107 container_limits: 108 description: 109 - A dictionary of limits applied to each container created by the build process. 110 type: dict 111 suboptions: 112 memory: 113 description: 114 - Set memory limit for build. 115 type: int 116 memswap: 117 description: 118 - Total memory (memory + swap), -1 to disable swap. 119 type: int 120 cpushares: 121 description: 122 - CPU shares (relative weight). 123 type: int 124 cpusetcpus: 125 description: 126 - CPUs in which to allow execution, e.g., "0-3", "0,1". 127 type: str 128 use_config_proxy: 129 description: 130 - If set to C(yes) and a proxy configuration is specified in the docker client configuration 131 (by default C($HOME/.docker/config.json)), the corresponding environment variables will 132 be set in the container being built. 133 - Needs Docker SDK for Python >= 3.7.0. 134 type: bool 135 target: 136 description: 137 - When building an image specifies an intermediate build stage by 138 name as a final stage for the resulting image. 139 type: str 140 version_added: "2.9" 141 version_added: "2.8" 142 archive_path: 143 description: 144 - Use with state C(present) to archive an image to a .tar file. 145 type: path 146 version_added: "2.1" 147 load_path: 148 description: 149 - Use with state C(present) to load an image from a .tar file. 150 - Set I(source) to C(load) if you want to load the image. The option will 151 be set automatically before Ansible 2.12 if this option is used (except 152 if I(path) is specified as well, in which case building will take precedence). 153 From Ansible 2.12 on, you have to set I(source) to C(load). 154 type: path 155 version_added: "2.2" 156 dockerfile: 157 description: 158 - Use with state C(present) and source C(build) to provide an alternate name for the Dockerfile to use when building an image. 159 - This can also include a relative path (relative to I(path)). 160 - Please use I(build.dockerfile) instead. This option will be removed in Ansible 2.12. 161 type: str 162 version_added: "2.0" 163 force: 164 description: 165 - Use with state I(absent) to un-tag and remove all images matching the specified name. Use with state 166 C(present) to build, load or pull an image when the image already exists. Also use with state C(present) 167 to force tagging an image. 168 - Please stop using this option, and use the more specialized force options 169 I(force_source), I(force_absent) and I(force_tag) instead. 170 - This option will be removed in Ansible 2.12. 171 type: bool 172 version_added: "2.1" 173 force_source: 174 description: 175 - Use with state C(present) to build, load or pull an image (depending on the 176 value of the I(source) option) when the image already exists. 177 type: bool 178 default: false 179 version_added: "2.8" 180 force_absent: 181 description: 182 - Use with state I(absent) to un-tag and remove all images matching the specified name. 183 type: bool 184 default: false 185 version_added: "2.8" 186 force_tag: 187 description: 188 - Use with state C(present) to force tagging an image. 189 type: bool 190 default: false 191 version_added: "2.8" 192 http_timeout: 193 description: 194 - Timeout for HTTP requests during the image build operation. Provide a positive integer value for the number of 195 seconds. 196 - Please use I(build.http_timeout) instead. This option will be removed in Ansible 2.12. 197 type: int 198 version_added: "2.1" 199 name: 200 description: 201 - "Image name. Name format will be one of: name, repository/name, registry_server:port/name. 202 When pushing or pulling an image the name can optionally include the tag by appending ':tag_name'." 203 - Note that image IDs (hashes) are not supported. 204 type: str 205 required: yes 206 path: 207 description: 208 - Use with state 'present' to build an image. Will be the path to a directory containing the context and 209 Dockerfile for building an image. 210 - Set I(source) to C(build) if you want to build the image. The option will 211 be set automatically before Ansible 2.12 if this option is used. From Ansible 2.12 212 on, you have to set I(source) to C(build). 213 - Please use I(build.path) instead. This option will be removed in Ansible 2.12. 214 type: path 215 aliases: 216 - build_path 217 pull: 218 description: 219 - When building an image downloads any updates to the FROM image in Dockerfile. 220 - Please use I(build.pull) instead. This option will be removed in Ansible 2.12. 221 - The default is currently C(yes). This will change to C(no) in Ansible 2.12. 222 type: bool 223 version_added: "2.1" 224 push: 225 description: 226 - Push the image to the registry. Specify the registry as part of the I(name) or I(repository) parameter. 227 type: bool 228 default: no 229 version_added: "2.2" 230 rm: 231 description: 232 - Remove intermediate containers after build. 233 - Please use I(build.rm) instead. This option will be removed in Ansible 2.12. 234 type: bool 235 default: yes 236 version_added: "2.1" 237 nocache: 238 description: 239 - Do not use cache when building an image. 240 - Please use I(build.nocache) instead. This option will be removed in Ansible 2.12. 241 type: bool 242 default: no 243 repository: 244 description: 245 - Full path to a repository. Use with state C(present) to tag the image into the repository. Expects 246 format I(repository:tag). If no tag is provided, will use the value of the C(tag) parameter or I(latest). 247 type: str 248 version_added: "2.1" 249 state: 250 description: 251 - Make assertions about the state of an image. 252 - When C(absent) an image will be removed. Use the force option to un-tag and remove all images 253 matching the provided name. 254 - When C(present) check if an image exists using the provided name and tag. If the image is not found or the 255 force option is used, the image will either be pulled, built or loaded, depending on the I(source) option. 256 - By default the image will be pulled from Docker Hub, or the registry specified in the image's name. Note that 257 this will change in Ansible 2.12, so to make sure that you are pulling, set I(source) to C(pull). To build 258 the image, provide a I(path) value set to a directory containing a context and Dockerfile, and set I(source) 259 to C(build). To load an image, specify I(load_path) to provide a path to an archive file. To tag an image to 260 a repository, provide a I(repository) path. If the name contains a repository path, it will be pushed. 261 - "*Note:* C(state=build) is DEPRECATED and will be removed in Ansible 2.11. Specifying C(build) will behave the 262 same as C(present)." 263 type: str 264 default: present 265 choices: 266 - absent 267 - present 268 - build 269 tag: 270 description: 271 - Used to select an image when pulling. Will be added to the image when pushing, tagging or building. Defaults to 272 I(latest). 273 - If I(name) parameter format is I(name:tag), then tag value from I(name) will take precedence. 274 type: str 275 default: latest 276 buildargs: 277 description: 278 - Provide a dictionary of C(key:value) build arguments that map to Dockerfile ARG directive. 279 - Docker expects the value to be a string. For convenience any non-string values will be converted to strings. 280 - Requires Docker API >= 1.21. 281 - Please use I(build.args) instead. This option will be removed in Ansible 2.12. 282 type: dict 283 version_added: "2.2" 284 container_limits: 285 description: 286 - A dictionary of limits applied to each container created by the build process. 287 - Please use I(build.container_limits) instead. This option will be removed in Ansible 2.12. 288 type: dict 289 suboptions: 290 memory: 291 description: 292 - Set memory limit for build. 293 type: int 294 memswap: 295 description: 296 - Total memory (memory + swap), -1 to disable swap. 297 type: int 298 cpushares: 299 description: 300 - CPU shares (relative weight). 301 type: int 302 cpusetcpus: 303 description: 304 - CPUs in which to allow execution, e.g., "0-3", "0,1". 305 type: str 306 version_added: "2.1" 307 use_tls: 308 description: 309 - "DEPRECATED. Whether to use tls to connect to the docker daemon. Set to 310 C(encrypt) to use TLS. And set to C(verify) to use TLS and verify that 311 the server's certificate is valid for the server." 312 - "*Note:* If you specify this option, it will set the value of the I(tls) or 313 I(validate_certs) parameters if not set to C(no)." 314 - Will be removed in Ansible 2.11. 315 type: str 316 choices: 317 - 'no' 318 - 'encrypt' 319 - 'verify' 320 version_added: "2.0" 321 322extends_documentation_fragment: 323 - docker 324 - docker.docker_py_1_documentation 325 326requirements: 327 - "L(Docker SDK for Python,https://docker-py.readthedocs.io/en/stable/) >= 1.8.0 (use L(docker-py,https://pypi.org/project/docker-py/) for Python 2.6)" 328 - "Docker API >= 1.20" 329 330author: 331 - Pavel Antonov (@softzilla) 332 - Chris Houseknecht (@chouseknecht) 333 - Sorin Sbarnea (@ssbarnea) 334 335''' 336 337EXAMPLES = ''' 338 339- name: pull an image 340 docker_image: 341 name: pacur/centos-7 342 source: pull 343 344- name: Tag and push to docker hub 345 docker_image: 346 name: pacur/centos-7:56 347 repository: dcoppenhagan/myimage:7.56 348 push: yes 349 source: local 350 351- name: Tag and push to local registry 352 docker_image: 353 # Image will be centos:7 354 name: centos 355 # Will be pushed to localhost:5000/centos:7 356 repository: localhost:5000/centos 357 tag: 7 358 push: yes 359 source: local 360 361- name: Add tag latest to image 362 docker_image: 363 name: myimage:7.1.2 364 repository: myimage:latest 365 # As 'latest' usually already is present, we need to enable overwriting of existing tags: 366 force_tag: yes 367 source: local 368 369- name: Remove image 370 docker_image: 371 state: absent 372 name: registry.ansible.com/chouseknecht/sinatra 373 tag: v1 374 375- name: Build an image and push it to a private repo 376 docker_image: 377 build: 378 path: ./sinatra 379 name: registry.ansible.com/chouseknecht/sinatra 380 tag: v1 381 push: yes 382 source: build 383 384- name: Archive image 385 docker_image: 386 name: registry.ansible.com/chouseknecht/sinatra 387 tag: v1 388 archive_path: my_sinatra.tar 389 source: local 390 391- name: Load image from archive and push to a private registry 392 docker_image: 393 name: localhost:5000/myimages/sinatra 394 tag: v1 395 push: yes 396 load_path: my_sinatra.tar 397 source: load 398 399- name: Build image and with build args 400 docker_image: 401 name: myimage 402 build: 403 path: /path/to/build/dir 404 args: 405 log_volume: /var/log/myapp 406 listen_port: 8080 407 source: build 408 409- name: Build image using cache source 410 docker_image: 411 name: myimage:latest 412 build: 413 path: /path/to/build/dir 414 # Use as cache source for building myimage 415 cache_from: 416 - nginx:latest 417 - alpine:3.8 418 source: build 419''' 420 421RETURN = ''' 422image: 423 description: Image inspection results for the affected image. 424 returned: success 425 type: dict 426 sample: {} 427''' 428 429import errno 430import os 431import re 432import traceback 433 434from distutils.version import LooseVersion 435 436from ansible.module_utils.docker.common import ( 437 clean_dict_booleans_for_docker_api, 438 docker_version, 439 AnsibleDockerClient, 440 DockerBaseClass, 441 is_image_name_id, 442 is_valid_tag, 443 RequestException, 444) 445from ansible.module_utils._text import to_native 446 447if docker_version is not None: 448 try: 449 if LooseVersion(docker_version) >= LooseVersion('2.0.0'): 450 from docker.auth import resolve_repository_name 451 else: 452 from docker.auth.auth import resolve_repository_name 453 from docker.utils.utils import parse_repository_tag 454 from docker.errors import DockerException, NotFound 455 except ImportError: 456 # missing Docker SDK for Python handled in module_utils.docker.common 457 pass 458 459 460class ImageManager(DockerBaseClass): 461 462 def __init__(self, client, results): 463 464 super(ImageManager, self).__init__() 465 466 self.client = client 467 self.results = results 468 parameters = self.client.module.params 469 self.check_mode = self.client.check_mode 470 471 self.source = parameters['source'] 472 build = parameters['build'] or dict() 473 self.archive_path = parameters.get('archive_path') 474 self.cache_from = build.get('cache_from') 475 self.container_limits = build.get('container_limits') 476 self.dockerfile = build.get('dockerfile') 477 self.force_source = parameters.get('force_source') 478 self.force_absent = parameters.get('force_absent') 479 self.force_tag = parameters.get('force_tag') 480 self.load_path = parameters.get('load_path') 481 self.name = parameters.get('name') 482 self.network = build.get('network') 483 self.extra_hosts = clean_dict_booleans_for_docker_api(build.get('etc_hosts')) 484 self.nocache = build.get('nocache', False) 485 self.build_path = build.get('path') 486 self.pull = build.get('pull') 487 self.target = build.get('target') 488 self.repository = parameters.get('repository') 489 self.rm = build.get('rm', True) 490 self.state = parameters.get('state') 491 self.tag = parameters.get('tag') 492 self.http_timeout = build.get('http_timeout') 493 self.push = parameters.get('push') 494 self.buildargs = build.get('args') 495 self.use_config_proxy = build.get('use_config_proxy') 496 497 # If name contains a tag, it takes precedence over tag parameter. 498 if not is_image_name_id(self.name): 499 repo, repo_tag = parse_repository_tag(self.name) 500 if repo_tag: 501 self.name = repo 502 self.tag = repo_tag 503 504 if self.state == 'present': 505 self.present() 506 elif self.state == 'absent': 507 self.absent() 508 509 def fail(self, msg): 510 self.client.fail(msg) 511 512 def present(self): 513 ''' 514 Handles state = 'present', which includes building, loading or pulling an image, 515 depending on user provided parameters. 516 517 :returns None 518 ''' 519 image = self.client.find_image(name=self.name, tag=self.tag) 520 521 if not image or self.force_source: 522 if self.source == 'build': 523 # Build the image 524 if not os.path.isdir(self.build_path): 525 self.fail("Requested build path %s could not be found or you do not have access." % self.build_path) 526 image_name = self.name 527 if self.tag: 528 image_name = "%s:%s" % (self.name, self.tag) 529 self.log("Building image %s" % image_name) 530 self.results['actions'].append("Built image %s from %s" % (image_name, self.build_path)) 531 self.results['changed'] = True 532 if not self.check_mode: 533 self.results['image'] = self.build_image() 534 elif self.source == 'load': 535 # Load the image from an archive 536 if not os.path.isfile(self.load_path): 537 self.fail("Error loading image %s. Specified path %s does not exist." % (self.name, 538 self.load_path)) 539 image_name = self.name 540 if self.tag: 541 image_name = "%s:%s" % (self.name, self.tag) 542 self.results['actions'].append("Loaded image %s from %s" % (image_name, self.load_path)) 543 self.results['changed'] = True 544 if not self.check_mode: 545 self.results['image'] = self.load_image() 546 elif self.source == 'pull': 547 # pull the image 548 self.results['actions'].append('Pulled image %s:%s' % (self.name, self.tag)) 549 self.results['changed'] = True 550 if not self.check_mode: 551 self.results['image'], dummy = self.client.pull_image(self.name, tag=self.tag) 552 elif self.source == 'local': 553 if image is None: 554 name = self.name 555 if self.tag: 556 name = "%s:%s" % (self.name, self.tag) 557 self.client.fail('Cannot find the image %s locally.' % name) 558 if not self.check_mode and image and image['Id'] == self.results['image']['Id']: 559 self.results['changed'] = False 560 else: 561 self.results['image'] = image 562 563 if self.archive_path: 564 self.archive_image(self.name, self.tag) 565 566 if self.push and not self.repository: 567 self.push_image(self.name, self.tag) 568 elif self.repository: 569 self.tag_image(self.name, self.tag, self.repository, push=self.push) 570 571 def absent(self): 572 ''' 573 Handles state = 'absent', which removes an image. 574 575 :return None 576 ''' 577 name = self.name 578 if is_image_name_id(name): 579 image = self.client.find_image_by_id(name, accept_missing_image=True) 580 else: 581 image = self.client.find_image(name, self.tag) 582 if self.tag: 583 name = "%s:%s" % (self.name, self.tag) 584 if image: 585 if not self.check_mode: 586 try: 587 self.client.remove_image(name, force=self.force_absent) 588 except NotFound: 589 # If the image vanished while we were trying to remove it, don't fail 590 pass 591 except Exception as exc: 592 self.fail("Error removing image %s - %s" % (name, str(exc))) 593 594 self.results['changed'] = True 595 self.results['actions'].append("Removed image %s" % (name)) 596 self.results['image']['state'] = 'Deleted' 597 598 def archive_image(self, name, tag): 599 ''' 600 Archive an image to a .tar file. Called when archive_path is passed. 601 602 :param name - name of the image. Type: str 603 :return None 604 ''' 605 606 if not tag: 607 tag = "latest" 608 609 image = self.client.find_image(name=name, tag=tag) 610 if not image: 611 self.log("archive image: image %s:%s not found" % (name, tag)) 612 return 613 614 image_name = "%s:%s" % (name, tag) 615 self.results['actions'].append('Archived image %s to %s' % (image_name, self.archive_path)) 616 self.results['changed'] = True 617 if not self.check_mode: 618 self.log("Getting archive of image %s" % image_name) 619 try: 620 image = self.client.get_image(image_name) 621 except Exception as exc: 622 self.fail("Error getting image %s - %s" % (image_name, str(exc))) 623 624 try: 625 with open(self.archive_path, 'wb') as fd: 626 if self.client.docker_py_version >= LooseVersion('3.0.0'): 627 for chunk in image: 628 fd.write(chunk) 629 else: 630 for chunk in image.stream(2048, decode_content=False): 631 fd.write(chunk) 632 except Exception as exc: 633 self.fail("Error writing image archive %s - %s" % (self.archive_path, str(exc))) 634 635 image = self.client.find_image(name=name, tag=tag) 636 if image: 637 self.results['image'] = image 638 639 def push_image(self, name, tag=None): 640 ''' 641 If the name of the image contains a repository path, then push the image. 642 643 :param name Name of the image to push. 644 :param tag Use a specific tag. 645 :return: None 646 ''' 647 648 repository = name 649 if not tag: 650 repository, tag = parse_repository_tag(name) 651 registry, repo_name = resolve_repository_name(repository) 652 653 self.log("push %s to %s/%s:%s" % (self.name, registry, repo_name, tag)) 654 655 if registry: 656 self.results['actions'].append("Pushed image %s to %s/%s:%s" % (self.name, registry, repo_name, tag)) 657 self.results['changed'] = True 658 if not self.check_mode: 659 status = None 660 try: 661 changed = False 662 for line in self.client.push(repository, tag=tag, stream=True, decode=True): 663 self.log(line, pretty_print=True) 664 if line.get('errorDetail'): 665 raise Exception(line['errorDetail']['message']) 666 status = line.get('status') 667 if status == 'Pushing': 668 changed = True 669 self.results['changed'] = changed 670 except Exception as exc: 671 if re.search('unauthorized', str(exc)): 672 if re.search('authentication required', str(exc)): 673 self.fail("Error pushing image %s/%s:%s - %s. Try logging into %s first." % 674 (registry, repo_name, tag, str(exc), registry)) 675 else: 676 self.fail("Error pushing image %s/%s:%s - %s. Does the repository exist?" % 677 (registry, repo_name, tag, str(exc))) 678 self.fail("Error pushing image %s: %s" % (repository, str(exc))) 679 self.results['image'] = self.client.find_image(name=repository, tag=tag) 680 if not self.results['image']: 681 self.results['image'] = dict() 682 self.results['image']['push_status'] = status 683 684 def tag_image(self, name, tag, repository, push=False): 685 ''' 686 Tag an image into a repository. 687 688 :param name: name of the image. required. 689 :param tag: image tag. 690 :param repository: path to the repository. required. 691 :param push: bool. push the image once it's tagged. 692 :return: None 693 ''' 694 repo, repo_tag = parse_repository_tag(repository) 695 if not repo_tag: 696 repo_tag = "latest" 697 if tag: 698 repo_tag = tag 699 image = self.client.find_image(name=repo, tag=repo_tag) 700 found = 'found' if image else 'not found' 701 self.log("image %s was %s" % (repo, found)) 702 703 if not image or self.force_tag: 704 self.log("tagging %s:%s to %s:%s" % (name, tag, repo, repo_tag)) 705 self.results['changed'] = True 706 self.results['actions'].append("Tagged image %s:%s to %s:%s" % (name, tag, repo, repo_tag)) 707 if not self.check_mode: 708 try: 709 # Finding the image does not always work, especially running a localhost registry. In those 710 # cases, if we don't set force=True, it errors. 711 image_name = name 712 if tag and not re.search(tag, name): 713 image_name = "%s:%s" % (name, tag) 714 tag_status = self.client.tag(image_name, repo, tag=repo_tag, force=True) 715 if not tag_status: 716 raise Exception("Tag operation failed.") 717 except Exception as exc: 718 self.fail("Error: failed to tag image - %s" % str(exc)) 719 self.results['image'] = self.client.find_image(name=repo, tag=repo_tag) 720 if image and image['Id'] == self.results['image']['Id']: 721 self.results['changed'] = False 722 723 if push: 724 self.push_image(repo, repo_tag) 725 726 def build_image(self): 727 ''' 728 Build an image 729 730 :return: image dict 731 ''' 732 params = dict( 733 path=self.build_path, 734 tag=self.name, 735 rm=self.rm, 736 nocache=self.nocache, 737 timeout=self.http_timeout, 738 pull=self.pull, 739 forcerm=self.rm, 740 dockerfile=self.dockerfile, 741 decode=True, 742 ) 743 if self.client.docker_py_version < LooseVersion('3.0.0'): 744 params['stream'] = True 745 build_output = [] 746 if self.tag: 747 params['tag'] = "%s:%s" % (self.name, self.tag) 748 if self.container_limits: 749 params['container_limits'] = self.container_limits 750 if self.buildargs: 751 for key, value in self.buildargs.items(): 752 self.buildargs[key] = to_native(value) 753 params['buildargs'] = self.buildargs 754 if self.cache_from: 755 params['cache_from'] = self.cache_from 756 if self.network: 757 params['network_mode'] = self.network 758 if self.extra_hosts: 759 params['extra_hosts'] = self.extra_hosts 760 if self.use_config_proxy: 761 params['use_config_proxy'] = self.use_config_proxy 762 # Due to a bug in docker-py, it will crash if 763 # use_config_proxy is True and buildargs is None 764 if 'buildargs' not in params: 765 params['buildargs'] = {} 766 if self.target: 767 params['target'] = self.target 768 769 for line in self.client.build(**params): 770 # line = json.loads(line) 771 self.log(line, pretty_print=True) 772 if "stream" in line: 773 build_output.append(line["stream"]) 774 if line.get('error'): 775 if line.get('errorDetail'): 776 errorDetail = line.get('errorDetail') 777 self.fail( 778 "Error building %s - code: %s, message: %s, logs: %s" % ( 779 self.name, 780 errorDetail.get('code'), 781 errorDetail.get('message'), 782 build_output)) 783 else: 784 self.fail("Error building %s - message: %s, logs: %s" % ( 785 self.name, line.get('error'), build_output)) 786 return self.client.find_image(name=self.name, tag=self.tag) 787 788 def load_image(self): 789 ''' 790 Load an image from a .tar archive 791 792 :return: image dict 793 ''' 794 # Load image(s) from file 795 load_output = [] 796 has_output = False 797 try: 798 self.log("Opening image %s" % self.load_path) 799 with open(self.load_path, 'rb') as image_tar: 800 self.log("Loading image from %s" % self.load_path) 801 output = self.client.load_image(image_tar) 802 if output is not None: 803 # Old versions of Docker SDK of Python (before version 2.5.0) do not return anything. 804 # (See https://github.com/docker/docker-py/commit/7139e2d8f1ea82340417add02090bfaf7794f159) 805 # Note that before that commit, something else than None was returned, but that was also 806 # only introduced in a commit that first appeared in 2.5.0 (see 807 # https://github.com/docker/docker-py/commit/9e793806ff79559c3bc591d8c52a3bbe3cdb7350). 808 # So the above check works for every released version of Docker SDK for Python. 809 has_output = True 810 for line in output: 811 self.log(line, pretty_print=True) 812 if "stream" in line or "status" in line: 813 load_line = line.get("stream") or line.get("status") or '' 814 load_output.append(load_line) 815 else: 816 if LooseVersion(docker_version) < LooseVersion('2.5.0'): 817 self.client.module.warn( 818 'The installed version of the Docker SDK for Python does not return the loading results' 819 ' from the Docker daemon. Therefore, we cannot verify whether the expected image was' 820 ' loaded, whether multiple images where loaded, or whether the load actually succeeded.' 821 ' If you are not stuck with Python 2.6, *please* upgrade to a version newer than 2.5.0' 822 ' (2.5.0 was released in August 2017).' 823 ) 824 else: 825 self.client.module.warn( 826 'The API version of your Docker daemon is < 1.23, which does not return the image' 827 ' loading result from the Docker daemon. Therefore, we cannot verify whether the' 828 ' expected image was loaded, whether multiple images where loaded, or whether the load' 829 ' actually succeeded. You should consider upgrading your Docker daemon.' 830 ) 831 except EnvironmentError as exc: 832 if exc.errno == errno.ENOENT: 833 self.client.fail("Error opening image %s - %s" % (self.load_path, str(exc))) 834 self.client.fail("Error loading image %s - %s" % (self.name, str(exc)), stdout='\n'.join(load_output)) 835 except Exception as exc: 836 self.client.fail("Error loading image %s - %s" % (self.name, str(exc)), stdout='\n'.join(load_output)) 837 838 # Collect loaded images 839 if has_output: 840 # We can only do this when we actually got some output from Docker daemon 841 loaded_images = set() 842 for line in load_output: 843 if line.startswith('Loaded image:'): 844 loaded_images.add(line[len('Loaded image:'):].strip()) 845 846 if not loaded_images: 847 self.client.fail("Detected no loaded images. Archive potentially corrupt?", stdout='\n'.join(load_output)) 848 849 expected_image = '%s:%s' % (self.name, self.tag) 850 if expected_image not in loaded_images: 851 self.client.fail( 852 "The archive did not contain image '%s'. Instead, found %s." % ( 853 expected_image, ', '.join(["'%s'" % image for image in sorted(loaded_images)])), 854 stdout='\n'.join(load_output)) 855 loaded_images.remove(expected_image) 856 857 if loaded_images: 858 self.client.module.warn( 859 "The archive contained more images than specified: %s" % ( 860 ', '.join(["'%s'" % image for image in sorted(loaded_images)]), )) 861 862 return self.client.find_image(self.name, self.tag) 863 864 865def main(): 866 argument_spec = dict( 867 source=dict(type='str', choices=['build', 'load', 'pull', 'local']), 868 build=dict(type='dict', options=dict( 869 cache_from=dict(type='list', elements='str'), 870 container_limits=dict(type='dict', options=dict( 871 memory=dict(type='int'), 872 memswap=dict(type='int'), 873 cpushares=dict(type='int'), 874 cpusetcpus=dict(type='str'), 875 )), 876 dockerfile=dict(type='str'), 877 http_timeout=dict(type='int'), 878 network=dict(type='str'), 879 nocache=dict(type='bool', default=False), 880 path=dict(type='path', required=True), 881 pull=dict(type='bool'), 882 rm=dict(type='bool', default=True), 883 args=dict(type='dict'), 884 use_config_proxy=dict(type='bool'), 885 target=dict(type='str'), 886 etc_hosts=dict(type='dict'), 887 )), 888 archive_path=dict(type='path'), 889 container_limits=dict(type='dict', options=dict( 890 memory=dict(type='int'), 891 memswap=dict(type='int'), 892 cpushares=dict(type='int'), 893 cpusetcpus=dict(type='str'), 894 ), removed_in_version='2.12'), 895 dockerfile=dict(type='str', removed_in_version='2.12'), 896 force=dict(type='bool', removed_in_version='2.12'), 897 force_source=dict(type='bool', default=False), 898 force_absent=dict(type='bool', default=False), 899 force_tag=dict(type='bool', default=False), 900 http_timeout=dict(type='int', removed_in_version='2.12'), 901 load_path=dict(type='path'), 902 name=dict(type='str', required=True), 903 nocache=dict(type='bool', default=False, removed_in_version='2.12'), 904 path=dict(type='path', aliases=['build_path'], removed_in_version='2.12'), 905 pull=dict(type='bool', removed_in_version='2.12'), 906 push=dict(type='bool', default=False), 907 repository=dict(type='str'), 908 rm=dict(type='bool', default=True, removed_in_version='2.12'), 909 state=dict(type='str', default='present', choices=['absent', 'present', 'build']), 910 tag=dict(type='str', default='latest'), 911 use_tls=dict(type='str', choices=['no', 'encrypt', 'verify'], removed_in_version='2.11'), 912 buildargs=dict(type='dict', removed_in_version='2.12'), 913 ) 914 915 required_if = [ 916 # ('state', 'present', ['source']), -- enable in Ansible 2.12. 917 # ('source', 'build', ['build']), -- enable in Ansible 2.12. 918 ('source', 'load', ['load_path']), 919 ] 920 921 def detect_build_cache_from(client): 922 return client.module.params['build'] and client.module.params['build'].get('cache_from') is not None 923 924 def detect_build_network(client): 925 return client.module.params['build'] and client.module.params['build'].get('network') is not None 926 927 def detect_build_target(client): 928 return client.module.params['build'] and client.module.params['build'].get('target') is not None 929 930 def detect_use_config_proxy(client): 931 return client.module.params['build'] and client.module.params['build'].get('use_config_proxy') is not None 932 933 def detect_etc_hosts(client): 934 return client.module.params['build'] and bool(client.module.params['build'].get('etc_hosts')) 935 936 option_minimal_versions = dict() 937 option_minimal_versions["build.cache_from"] = dict(docker_py_version='2.1.0', docker_api_version='1.25', detect_usage=detect_build_cache_from) 938 option_minimal_versions["build.network"] = dict(docker_py_version='2.4.0', docker_api_version='1.25', detect_usage=detect_build_network) 939 option_minimal_versions["build.target"] = dict(docker_py_version='2.4.0', detect_usage=detect_build_target) 940 option_minimal_versions["build.use_config_proxy"] = dict(docker_py_version='3.7.0', detect_usage=detect_use_config_proxy) 941 option_minimal_versions["build.etc_hosts"] = dict(docker_py_version='2.6.0', docker_api_version='1.27', detect_usage=detect_etc_hosts) 942 943 client = AnsibleDockerClient( 944 argument_spec=argument_spec, 945 required_if=required_if, 946 supports_check_mode=True, 947 min_docker_version='1.8.0', 948 min_docker_api_version='1.20', 949 option_minimal_versions=option_minimal_versions, 950 ) 951 952 if client.module.params['state'] == 'build': 953 client.module.warn('The "build" state has been deprecated for a long time ' 954 'and will be removed in Ansible 2.11. Please use ' 955 '"present", which has the same meaning as "build".') 956 client.module.params['state'] = 'present' 957 if client.module.params['use_tls']: 958 client.module.warn('The "use_tls" option has been deprecated for a long time ' 959 'and will be removed in Ansible 2.11. Please use the' 960 '"tls" and "validate_certs" options instead.') 961 962 if not is_valid_tag(client.module.params['tag'], allow_empty=True): 963 client.fail('"{0}" is not a valid docker tag!'.format(client.module.params['tag'])) 964 965 build_options = dict( 966 container_limits='container_limits', 967 dockerfile='dockerfile', 968 http_timeout='http_timeout', 969 nocache='nocache', 970 path='path', 971 pull='pull', 972 rm='rm', 973 buildargs='args', 974 ) 975 for option, build_option in build_options.items(): 976 default_value = None 977 if option in ('rm', ): 978 default_value = True 979 elif option in ('nocache', ): 980 default_value = False 981 if client.module.params[option] != default_value: 982 if client.module.params['build'] is None: 983 client.module.params['build'] = dict() 984 if client.module.params['build'].get(build_option, default_value) != default_value: 985 client.fail('Cannot specify both %s and build.%s!' % (option, build_option)) 986 client.module.params['build'][build_option] = client.module.params[option] 987 client.module.warn('Please specify build.%s instead of %s. The %s option ' 988 'has been renamed and will be removed in Ansible 2.12.' % (build_option, option, option)) 989 if client.module.params['source'] == 'build': 990 if (not client.module.params['build'] or not client.module.params['build'].get('path')): 991 client.fail('If "source" is set to "build", the "build.path" option must be specified.') 992 if client.module.params['build'].get('pull') is None: 993 client.module.warn("The default for build.pull is currently 'yes', but will be changed to 'no' in Ansible 2.12. " 994 "Please set build.pull explicitly to the value you need.") 995 client.module.params['build']['pull'] = True # TODO: change to False in Ansible 2.12 996 997 if client.module.params['state'] == 'present' and client.module.params['source'] is None: 998 # Autodetection. To be removed in Ansible 2.12. 999 if (client.module.params['build'] or dict()).get('path'): 1000 client.module.params['source'] = 'build' 1001 elif client.module.params['load_path']: 1002 client.module.params['source'] = 'load' 1003 else: 1004 client.module.params['source'] = 'pull' 1005 client.module.warn('The value of the "source" option was determined to be "%s". ' 1006 'Please set the "source" option explicitly. Autodetection will ' 1007 'be removed in Ansible 2.12.' % client.module.params['source']) 1008 1009 if client.module.params['force']: 1010 client.module.params['force_source'] = True 1011 client.module.params['force_absent'] = True 1012 client.module.params['force_tag'] = True 1013 client.module.warn('The "force" option will be removed in Ansible 2.12. Please ' 1014 'use the "force_source", "force_absent" or "force_tag" option ' 1015 'instead, depending on what you want to force.') 1016 1017 try: 1018 results = dict( 1019 changed=False, 1020 actions=[], 1021 image={} 1022 ) 1023 1024 ImageManager(client, results) 1025 client.module.exit_json(**results) 1026 except DockerException as e: 1027 client.fail('An unexpected docker error occurred: {0}'.format(e), exception=traceback.format_exc()) 1028 except RequestException as e: 1029 client.fail('An unexpected requests error occurred when docker-py tried to talk to the docker daemon: {0}'.format(e), exception=traceback.format_exc()) 1030 1031 1032if __name__ == '__main__': 1033 main() 1034