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