summaryrefslogtreecommitdiffstats
path: root/collections-debian-merged/ansible_collections/community/docker/plugins/modules/docker_image.py
blob: 07728813a3e253945ca0c04e36a9f0d107224034 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
#!/usr/bin/python
#
# Copyright 2016 Red Hat | Ansible
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)

from __future__ import absolute_import, division, print_function
__metaclass__ = type


DOCUMENTATION = '''
---
module: docker_image

short_description: Manage docker images.


description:
  - Build, load or pull an image, making the image available for creating containers. Also supports tagging an
    image into a repository and archiving an image to a C(.tar) file.

options:
  source:
    description:
      - "Determines where the module will try to retrieve the image from."
      - "Use C(build) to build the image from a C(Dockerfile). I(build.path) must
         be specified when this value is used."
      - "Use C(load) to load the image from a C(.tar) file. I(load_path) must
         be specified when this value is used."
      - "Use C(pull) to pull the image from a registry."
      - "Use C(local) to make sure that the image is already available on the local
         docker daemon, i.e. do not try to build, pull or load the image."
    type: str
    choices:
    - build
    - load
    - pull
    - local
  build:
    description:
      - "Specifies options used for building images."
    type: dict
    suboptions:
      cache_from:
        description:
          - List of image names to consider as cache source.
        type: list
        elements: str
      dockerfile:
        description:
          - Use with state C(present) and source C(build) to provide an alternate name for the Dockerfile to use when building an image.
          - This can also include a relative path (relative to I(path)).
        type: str
      http_timeout:
        description:
          - Timeout for HTTP requests during the image build operation. Provide a positive integer value for the number of
            seconds.
        type: int
      path:
        description:
          - Use with state 'present' to build an image. Will be the path to a directory containing the context and
            Dockerfile for building an image.
        type: path
        required: yes
      pull:
        description:
          - When building an image downloads any updates to the FROM image in Dockerfile.
        type: bool
        default: no
      rm:
        description:
          - Remove intermediate containers after build.
        type: bool
        default: yes
      network:
        description:
          - The network to use for C(RUN) build instructions.
        type: str
      nocache:
        description:
          - Do not use cache when building an image.
        type: bool
        default: no
      etc_hosts:
        description:
          - Extra hosts to add to C(/etc/hosts) in building containers, as a mapping of hostname to IP address.
        type: dict
      args:
        description:
          - Provide a dictionary of C(key:value) build arguments that map to Dockerfile ARG directive.
          - Docker expects the value to be a string. For convenience any non-string values will be converted to strings.
          - Requires Docker API >= 1.21.
        type: dict
      container_limits:
        description:
          - A dictionary of limits applied to each container created by the build process.
        type: dict
        suboptions:
          memory:
            description:
              - Set memory limit for build.
            type: int
          memswap:
            description:
              - Total memory (memory + swap), -1 to disable swap.
            type: int
          cpushares:
            description:
              - CPU shares (relative weight).
            type: int
          cpusetcpus:
            description:
              - CPUs in which to allow execution, e.g., "0-3", "0,1".
            type: str
      use_config_proxy:
        description:
          - If set to C(yes) and a proxy configuration is specified in the docker client configuration
            (by default C($HOME/.docker/config.json)), the corresponding environment variables will
            be set in the container being built.
          - Needs Docker SDK for Python >= 3.7.0.
        type: bool
      target:
        description:
          - When building an image specifies an intermediate build stage by
            name as a final stage for the resulting image.
        type: str
      platform:
        description:
          - Platform in the format C(os[/arch[/variant]]).
        type: str
        version_added: 1.1.0
  archive_path:
    description:
      - Use with state C(present) to archive an image to a .tar file.
    type: path
  load_path:
    description:
      - Use with state C(present) to load an image from a .tar file.
      - Set I(source) to C(load) if you want to load the image.
    type: path
  force_source:
    description:
      - Use with state C(present) to build, load or pull an image (depending on the
        value of the I(source) option) when the image already exists.
    type: bool
    default: false
  force_absent:
    description:
      - Use with state I(absent) to un-tag and remove all images matching the specified name.
    type: bool
    default: false
  force_tag:
    description:
      - Use with state C(present) to force tagging an image.
    type: bool
    default: false
  name:
    description:
      - "Image name. Name format will be one of: name, repository/name, registry_server:port/name.
        When pushing or pulling an image the name can optionally include the tag by appending ':tag_name'."
      - Note that image IDs (hashes) are not supported.
    type: str
    required: yes
  push:
    description:
      - Push the image to the registry. Specify the registry as part of the I(name) or I(repository) parameter.
    type: bool
    default: no
  repository:
    description:
      - Full path to a repository. Use with state C(present) to tag the image into the repository. Expects
        format I(repository:tag). If no tag is provided, will use the value of the C(tag) parameter or I(latest).
    type: str
  state:
    description:
      - Make assertions about the state of an image.
      - When C(absent) an image will be removed. Use the force option to un-tag and remove all images
        matching the provided name.
      - When C(present) check if an image exists using the provided name and tag. If the image is not found or the
        force option is used, the image will either be pulled, built or loaded, depending on the I(source) option.
    type: str
    default: present
    choices:
      - absent
      - present
  tag:
    description:
      - Used to select an image when pulling. Will be added to the image when pushing, tagging or building. Defaults to
        I(latest).
      - If I(name) parameter format is I(name:tag), then tag value from I(name) will take precedence.
    type: str
    default: latest

extends_documentation_fragment:
- community.docker.docker
- community.docker.docker.docker_py_1_documentation


requirements:
  - "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)"
  - "Docker API >= 1.20"

author:
  - Pavel Antonov (@softzilla)
  - Chris Houseknecht (@chouseknecht)
  - Sorin Sbarnea (@ssbarnea)

'''

EXAMPLES = '''

- name: Pull an image
  community.docker.docker_image:
    name: pacur/centos-7
    source: pull

- name: Tag and push to docker hub
  community.docker.docker_image:
    name: pacur/centos-7:56
    repository: dcoppenhagan/myimage:7.56
    push: yes
    source: local

- name: Tag and push to local registry
  community.docker.docker_image:
    # Image will be centos:7
    name: centos
    # Will be pushed to localhost:5000/centos:7
    repository: localhost:5000/centos
    tag: 7
    push: yes
    source: local

- name: Add tag latest to image
  community.docker.docker_image:
    name: myimage:7.1.2
    repository: myimage:latest
    # As 'latest' usually already is present, we need to enable overwriting of existing tags:
    force_tag: yes
    source: local

- name: Remove image
  community.docker.docker_image:
    state: absent
    name: registry.ansible.com/chouseknecht/sinatra
    tag: v1

- name: Build an image and push it to a private repo
  community.docker.docker_image:
    build:
      path: ./sinatra
    name: registry.ansible.com/chouseknecht/sinatra
    tag: v1
    push: yes
    source: build

- name: Archive image
  community.docker.docker_image:
    name: registry.ansible.com/chouseknecht/sinatra
    tag: v1
    archive_path: my_sinatra.tar
    source: local

- name: Load image from archive and push to a private registry
  community.docker.docker_image:
    name: localhost:5000/myimages/sinatra
    tag: v1
    push: yes
    load_path: my_sinatra.tar
    source: load

- name: Build image and with build args
  community.docker.docker_image:
    name: myimage
    build:
      path: /path/to/build/dir
      args:
        log_volume: /var/log/myapp
        listen_port: 8080
    source: build

- name: Build image using cache source
  community.docker.docker_image:
    name: myimage:latest
    build:
      path: /path/to/build/dir
      # Use as cache source for building myimage
      cache_from:
        - nginx:latest
        - alpine:3.8
    source: build
'''

RETURN = '''
image:
    description: Image inspection results for the affected image.
    returned: success
    type: dict
    sample: {}
stdout:
    description: Docker build output when building an image.
    returned: success
    type: str
    sample: ""
    version_added: 1.0.0
'''

import errno
import os
import re
import traceback

from distutils.version import LooseVersion

from ansible_collections.community.docker.plugins.module_utils.common import (
    clean_dict_booleans_for_docker_api,
    docker_version,
    AnsibleDockerClient,
    DockerBaseClass,
    is_image_name_id,
    is_valid_tag,
    RequestException,
)
from ansible.module_utils._text import to_native

if docker_version is not None:
    try:
        if LooseVersion(docker_version) >= LooseVersion('2.0.0'):
            from docker.auth import resolve_repository_name
        else:
            from docker.auth.auth import resolve_repository_name
        from docker.utils.utils import parse_repository_tag
        from docker.errors import DockerException
    except ImportError:
        # missing Docker SDK for Python handled in module_utils.docker.common
        pass


class ImageManager(DockerBaseClass):

    def __init__(self, client, results):

        super(ImageManager, self).__init__()

        self.client = client
        self.results = results
        parameters = self.client.module.params
        self.check_mode = self.client.check_mode

        self.source = parameters['source']
        build = parameters['build'] or dict()
        self.archive_path = parameters.get('archive_path')
        self.cache_from = build.get('cache_from')
        self.container_limits = build.get('container_limits')
        self.dockerfile = build.get('dockerfile')
        self.force_source = parameters.get('force_source')
        self.force_absent = parameters.get('force_absent')
        self.force_tag = parameters.get('force_tag')
        self.load_path = parameters.get('load_path')
        self.name = parameters.get('name')
        self.network = build.get('network')
        self.extra_hosts = clean_dict_booleans_for_docker_api(build.get('etc_hosts'))
        self.nocache = build.get('nocache', False)
        self.build_path = build.get('path')
        self.pull = build.get('pull')
        self.target = build.get('target')
        self.repository = parameters.get('repository')
        self.rm = build.get('rm', True)
        self.state = parameters.get('state')
        self.tag = parameters.get('tag')
        self.http_timeout = build.get('http_timeout')
        self.push = parameters.get('push')
        self.buildargs = build.get('args')
        self.build_platform = build.get('platform')
        self.use_config_proxy = build.get('use_config_proxy')

        # If name contains a tag, it takes precedence over tag parameter.
        if not is_image_name_id(self.name):
            repo, repo_tag = parse_repository_tag(self.name)
            if repo_tag:
                self.name = repo
                self.tag = repo_tag

        if self.state == 'present':
            self.present()
        elif self.state == 'absent':
            self.absent()

    def fail(self, msg):
        self.client.fail(msg)

    def present(self):
        '''
        Handles state = 'present', which includes building, loading or pulling an image,
        depending on user provided parameters.

        :returns None
        '''
        image = self.client.find_image(name=self.name, tag=self.tag)

        if not image or self.force_source:
            if self.source == 'build':
                # Build the image
                if not os.path.isdir(self.build_path):
                    self.fail("Requested build path %s could not be found or you do not have access." % self.build_path)
                image_name = self.name
                if self.tag:
                    image_name = "%s:%s" % (self.name, self.tag)
                self.log("Building image %s" % image_name)
                self.results['actions'].append("Built image %s from %s" % (image_name, self.build_path))
                self.results['changed'] = True
                if not self.check_mode:
                    self.results.update(self.build_image())

            elif self.source == 'load':
                # Load the image from an archive
                if not os.path.isfile(self.load_path):
                    self.fail("Error loading image %s. Specified path %s does not exist." % (self.name,
                                                                                             self.load_path))
                image_name = self.name
                if self.tag:
                    image_name = "%s:%s" % (self.name, self.tag)
                self.results['actions'].append("Loaded image %s from %s" % (image_name, self.load_path))
                self.results['changed'] = True
                if not self.check_mode:
                    self.results['image'] = self.load_image()
            elif self.source == 'pull':
                # pull the image
                self.results['actions'].append('Pulled image %s:%s' % (self.name, self.tag))
                self.results['changed'] = True
                if not self.check_mode:
                    self.results['image'], dummy = self.client.pull_image(self.name, tag=self.tag)
            elif self.source == 'local':
                if image is None:
                    name = self.name
                    if self.tag:
                        name = "%s:%s" % (self.name, self.tag)
                    self.client.fail('Cannot find the image %s locally.' % name)
            if not self.check_mode and image and image['Id'] == self.results['image']['Id']:
                self.results['changed'] = False

        if self.archive_path:
            self.archive_image(self.name, self.tag)

        if self.push and not self.repository:
            self.push_image(self.name, self.tag)
        elif self.repository:
            self.tag_image(self.name, self.tag, self.repository, push=self.push)

    def absent(self):
        '''
        Handles state = 'absent', which removes an image.

        :return None
        '''
        name = self.name
        if is_image_name_id(name):
            image = self.client.find_image_by_id(name)
        else:
            image = self.client.find_image(name, self.tag)
            if self.tag:
                name = "%s:%s" % (self.name, self.tag)
        if image:
            if not self.check_mode:
                try:
                    self.client.remove_image(name, force=self.force_absent)
                except Exception as exc:
                    self.fail("Error removing image %s - %s" % (name, str(exc)))

            self.results['changed'] = True
            self.results['actions'].append("Removed image %s" % (name))
            self.results['image']['state'] = 'Deleted'

    def archive_image(self, name, tag):
        '''
        Archive an image to a .tar file. Called when archive_path is passed.

        :param name - name of the image. Type: str
        :return None
        '''

        if not tag:
            tag = "latest"

        image = self.client.find_image(name=name, tag=tag)
        if not image:
            self.log("archive image: image %s:%s not found" % (name, tag))
            return

        image_name = "%s:%s" % (name, tag)
        self.results['actions'].append('Archived image %s to %s' % (image_name, self.archive_path))
        self.results['changed'] = True
        if not self.check_mode:
            self.log("Getting archive of image %s" % image_name)
            try:
                image = self.client.get_image(image_name)
            except Exception as exc:
                self.fail("Error getting image %s - %s" % (image_name, str(exc)))

            try:
                with open(self.archive_path, 'wb') as fd:
                    if self.client.docker_py_version >= LooseVersion('3.0.0'):
                        for chunk in image:
                            fd.write(chunk)
                    else:
                        for chunk in image.stream(2048, decode_content=False):
                            fd.write(chunk)
            except Exception as exc:
                self.fail("Error writing image archive %s - %s" % (self.archive_path, str(exc)))

        image = self.client.find_image(name=name, tag=tag)
        if image:
            self.results['image'] = image

    def push_image(self, name, tag=None):
        '''
        If the name of the image contains a repository path, then push the image.

        :param name Name of the image to push.
        :param tag Use a specific tag.
        :return: None
        '''

        repository = name
        if not tag:
            repository, tag = parse_repository_tag(name)
        registry, repo_name = resolve_repository_name(repository)

        self.log("push %s to %s/%s:%s" % (self.name, registry, repo_name, tag))

        if registry:
            self.results['actions'].append("Pushed image %s to %s/%s:%s" % (self.name, registry, repo_name, tag))
            self.results['changed'] = True
            if not self.check_mode:
                status = None
                try:
                    changed = False
                    for line in self.client.push(repository, tag=tag, stream=True, decode=True):
                        self.log(line, pretty_print=True)
                        if line.get('errorDetail'):
                            raise Exception(line['errorDetail']['message'])
                        status = line.get('status')
                        if status == 'Pushing':
                            changed = True
                    self.results['changed'] = changed
                except Exception as exc:
                    if re.search('unauthorized', str(exc)):
                        if re.search('authentication required', str(exc)):
                            self.fail("Error pushing image %s/%s:%s - %s. Try logging into %s first." %
                                      (registry, repo_name, tag, str(exc), registry))
                        else:
                            self.fail("Error pushing image %s/%s:%s - %s. Does the repository exist?" %
                                      (registry, repo_name, tag, str(exc)))
                    self.fail("Error pushing image %s: %s" % (repository, str(exc)))
                self.results['image'] = self.client.find_image(name=repository, tag=tag)
                if not self.results['image']:
                    self.results['image'] = dict()
                self.results['image']['push_status'] = status

    def tag_image(self, name, tag, repository, push=False):
        '''
        Tag an image into a repository.

        :param name: name of the image. required.
        :param tag: image tag.
        :param repository: path to the repository. required.
        :param push: bool. push the image once it's tagged.
        :return: None
        '''
        repo, repo_tag = parse_repository_tag(repository)
        if not repo_tag:
            repo_tag = "latest"
            if tag:
                repo_tag = tag
        image = self.client.find_image(name=repo, tag=repo_tag)
        found = 'found' if image else 'not found'
        self.log("image %s was %s" % (repo, found))

        if not image or self.force_tag:
            self.log("tagging %s:%s to %s:%s" % (name, tag, repo, repo_tag))
            self.results['changed'] = True
            self.results['actions'].append("Tagged image %s:%s to %s:%s" % (name, tag, repo, repo_tag))
            if not self.check_mode:
                try:
                    # Finding the image does not always work, especially running a localhost registry. In those
                    # cases, if we don't set force=True, it errors.
                    image_name = name
                    if tag and not re.search(tag, name):
                        image_name = "%s:%s" % (name, tag)
                    tag_status = self.client.tag(image_name, repo, tag=repo_tag, force=True)
                    if not tag_status:
                        raise Exception("Tag operation failed.")
                except Exception as exc:
                    self.fail("Error: failed to tag image - %s" % str(exc))
                self.results['image'] = self.client.find_image(name=repo, tag=repo_tag)
                if image and image['Id'] == self.results['image']['Id']:
                    self.results['changed'] = False

        if push:
            self.push_image(repo, repo_tag)

    @staticmethod
    def _extract_output_line(line, output):
        '''
        Extract text line from stream output and, if found, adds it to output.
        '''
        if 'stream' in line or 'status' in line:
            # Make sure we have a string (assuming that line['stream'] and
            # line['status'] are either not defined, falsish, or a string)
            text_line = line.get('stream') or line.get('status') or ''
            output.append(text_line)

    def build_image(self):
        '''
        Build an image

        :return: image dict
        '''
        params = dict(
            path=self.build_path,
            tag=self.name,
            rm=self.rm,
            nocache=self.nocache,
            timeout=self.http_timeout,
            pull=self.pull,
            forcerm=self.rm,
            dockerfile=self.dockerfile,
            decode=True,
        )
        if self.client.docker_py_version < LooseVersion('3.0.0'):
            params['stream'] = True

        if self.tag:
            params['tag'] = "%s:%s" % (self.name, self.tag)
        if self.container_limits:
            params['container_limits'] = self.container_limits
        if self.buildargs:
            for key, value in self.buildargs.items():
                self.buildargs[key] = to_native(value)
            params['buildargs'] = self.buildargs
        if self.cache_from:
            params['cache_from'] = self.cache_from
        if self.network:
            params['network_mode'] = self.network
        if self.extra_hosts:
            params['extra_hosts'] = self.extra_hosts
        if self.use_config_proxy:
            params['use_config_proxy'] = self.use_config_proxy
            # Due to a bug in docker-py, it will crash if
            # use_config_proxy is True and buildargs is None
            if 'buildargs' not in params:
                params['buildargs'] = {}
        if self.target:
            params['target'] = self.target
        if self.build_platform is not None:
            params['platform'] = self.build_platform

        build_output = []
        for line in self.client.build(**params):
            # line = json.loads(line)
            self.log(line, pretty_print=True)
            self._extract_output_line(line, build_output)

            if line.get('error'):
                if line.get('errorDetail'):
                    errorDetail = line.get('errorDetail')
                    self.fail(
                        "Error building %s - code: %s, message: %s, logs: %s" % (
                            self.name,
                            errorDetail.get('code'),
                            errorDetail.get('message'),
                            build_output))
                else:
                    self.fail("Error building %s - message: %s, logs: %s" % (
                        self.name, line.get('error'), build_output))

        return {"stdout": "\n".join(build_output),
                "image": self.client.find_image(name=self.name, tag=self.tag)}

    def load_image(self):
        '''
        Load an image from a .tar archive

        :return: image dict
        '''
        # Load image(s) from file
        load_output = []
        has_output = False
        try:
            self.log("Opening image %s" % self.load_path)
            with open(self.load_path, 'rb') as image_tar:
                self.log("Loading image from %s" % self.load_path)
                output = self.client.load_image(image_tar)
                if output is not None:
                    # Old versions of Docker SDK of Python (before version 2.5.0) do not return anything.
                    # (See https://github.com/docker/docker-py/commit/7139e2d8f1ea82340417add02090bfaf7794f159)
                    # Note that before that commit, something else than None was returned, but that was also
                    # only introduced in a commit that first appeared in 2.5.0 (see
                    # https://github.com/docker/docker-py/commit/9e793806ff79559c3bc591d8c52a3bbe3cdb7350).
                    # So the above check works for every released version of Docker SDK for Python.
                    has_output = True
                    for line in output:
                        self.log(line, pretty_print=True)
                        self._extract_output_line(line, load_output)
                else:
                    if LooseVersion(docker_version) < LooseVersion('2.5.0'):
                        self.client.module.warn(
                            'The installed version of the Docker SDK for Python does not return the loading results'
                            ' from the Docker daemon. Therefore, we cannot verify whether the expected image was'
                            ' loaded, whether multiple images where loaded, or whether the load actually succeeded.'
                            ' If you are not stuck with Python 2.6, *please* upgrade to a version newer than 2.5.0'
                            ' (2.5.0 was released in August 2017).'
                        )
                    else:
                        self.client.module.warn(
                            'The API version of your Docker daemon is < 1.23, which does not return the image'
                            ' loading result from the Docker daemon. Therefore, we cannot verify whether the'
                            ' expected image was loaded, whether multiple images where loaded, or whether the load'
                            ' actually succeeded. You should consider upgrading your Docker daemon.'
                        )
        except EnvironmentError as exc:
            if exc.errno == errno.ENOENT:
                self.client.fail("Error opening image %s - %s" % (self.load_path, str(exc)))
            self.client.fail("Error loading image %s - %s" % (self.name, str(exc)), stdout='\n'.join(load_output))
        except Exception as exc:
            self.client.fail("Error loading image %s - %s" % (self.name, str(exc)), stdout='\n'.join(load_output))

        # Collect loaded images
        if has_output:
            # We can only do this when we actually got some output from Docker daemon
            loaded_images = set()
            for line in load_output:
                if line.startswith('Loaded image:'):
                    loaded_images.add(line[len('Loaded image:'):].strip())

            if not loaded_images:
                self.client.fail("Detected no loaded images. Archive potentially corrupt?", stdout='\n'.join(load_output))

            expected_image = '%s:%s' % (self.name, self.tag)
            if expected_image not in loaded_images:
                self.client.fail(
                    "The archive did not contain image '%s'. Instead, found %s." % (
                        expected_image, ', '.join(["'%s'" % image for image in sorted(loaded_images)])),
                    stdout='\n'.join(load_output))
            loaded_images.remove(expected_image)

            if loaded_images:
                self.client.module.warn(
                    "The archive contained more images than specified: %s" % (
                        ', '.join(["'%s'" % image for image in sorted(loaded_images)]), ))

        return self.client.find_image(self.name, self.tag)


def main():
    argument_spec = dict(
        source=dict(type='str', choices=['build', 'load', 'pull', 'local']),
        build=dict(type='dict', options=dict(
            cache_from=dict(type='list', elements='str'),
            container_limits=dict(type='dict', options=dict(
                memory=dict(type='int'),
                memswap=dict(type='int'),
                cpushares=dict(type='int'),
                cpusetcpus=dict(type='str'),
            )),
            dockerfile=dict(type='str'),
            http_timeout=dict(type='int'),
            network=dict(type='str'),
            nocache=dict(type='bool', default=False),
            path=dict(type='path', required=True),
            pull=dict(type='bool', default=False),
            rm=dict(type='bool', default=True),
            args=dict(type='dict'),
            use_config_proxy=dict(type='bool'),
            target=dict(type='str'),
            etc_hosts=dict(type='dict'),
            platform=dict(type='str'),
        )),
        archive_path=dict(type='path'),
        force_source=dict(type='bool', default=False),
        force_absent=dict(type='bool', default=False),
        force_tag=dict(type='bool', default=False),
        load_path=dict(type='path'),
        name=dict(type='str', required=True),
        push=dict(type='bool', default=False),
        repository=dict(type='str'),
        state=dict(type='str', default='present', choices=['absent', 'present']),
        tag=dict(type='str', default='latest'),
    )

    required_if = [
        ('state', 'present', ['source']),
        ('source', 'build', ['build']),
        ('source', 'load', ['load_path']),
    ]

    def detect_build_cache_from(client):
        return client.module.params['build'] and client.module.params['build'].get('cache_from') is not None

    def detect_build_network(client):
        return client.module.params['build'] and client.module.params['build'].get('network') is not None

    def detect_build_target(client):
        return client.module.params['build'] and client.module.params['build'].get('target') is not None

    def detect_use_config_proxy(client):
        return client.module.params['build'] and client.module.params['build'].get('use_config_proxy') is not None

    def detect_etc_hosts(client):
        return client.module.params['build'] and bool(client.module.params['build'].get('etc_hosts'))

    def detect_platform(client):
        return client.module.params['build'] and client.module.params['build'].get('platform') is not None

    option_minimal_versions = dict()
    option_minimal_versions["build.cache_from"] = dict(docker_py_version='2.1.0', docker_api_version='1.25', detect_usage=detect_build_cache_from)
    option_minimal_versions["build.network"] = dict(docker_py_version='2.4.0', docker_api_version='1.25', detect_usage=detect_build_network)
    option_minimal_versions["build.target"] = dict(docker_py_version='2.4.0', detect_usage=detect_build_target)
    option_minimal_versions["build.use_config_proxy"] = dict(docker_py_version='3.7.0', detect_usage=detect_use_config_proxy)
    option_minimal_versions["build.etc_hosts"] = dict(docker_py_version='2.6.0', docker_api_version='1.27', detect_usage=detect_etc_hosts)
    option_minimal_versions["build.platform"] = dict(docker_py_version='3.0.0', docker_api_version='1.32', detect_usage=detect_platform)

    client = AnsibleDockerClient(
        argument_spec=argument_spec,
        required_if=required_if,
        supports_check_mode=True,
        min_docker_version='1.8.0',
        min_docker_api_version='1.20',
        option_minimal_versions=option_minimal_versions,
    )

    if not is_valid_tag(client.module.params['tag'], allow_empty=True):
        client.fail('"{0}" is not a valid docker tag!'.format(client.module.params['tag']))

    if client.module.params['source'] == 'build':
        if not client.module.params['build'] or not client.module.params['build'].get('path'):
            client.fail('If "source" is set to "build", the "build.path" option must be specified.')

    try:
        results = dict(
            changed=False,
            actions=[],
            image={}
        )

        ImageManager(client, results)
        client.module.exit_json(**results)
    except DockerException as e:
        client.fail('An unexpected docker error occurred: {0}'.format(e), exception=traceback.format_exc())
    except RequestException as e:
        client.fail('An unexpected requests error occurred when docker-py tried to talk to the docker daemon: {0}'.format(e), exception=traceback.format_exc())


if __name__ == '__main__':
    main()