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() pull = parameters['pull'] or dict() self.archive_path = parameters['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['force_source'] self.force_absent = parameters['force_absent'] self.force_tag = parameters['force_tag'] self.load_path = parameters['load_path'] self.name = parameters['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['repository'] self.rm = build.get('rm', True) self.state = parameters['state'] self.tag = parameters['tag'] self.http_timeout = build.get('http_timeout') self.pull_platform = pull.get('platform') self.push = parameters['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 # Sanity check: fail early when we know that something will fail later if self.repository and is_image_name_id(self.repository): self.fail("`repository` must not be an image ID; got: %s" % self.repository) if not self.repository and self.push and is_image_name_id(self.name): self.fail( "Cannot push an image by ID; specify `repository` to tag and push the image with ID %s instead" % self.name) if self.state == 'present': self.present() elif self.state == 'absent': self.absent()
def get_facts(self): ''' Lookup and inspect each image name found in the names parameter. :returns array of image dictionaries ''' results = [] names = self.name if not isinstance(names, list): names = [names] for name in names: if is_image_name_id(name): self.log('Fetching image %s (ID)' % (name)) image = self.client.find_image_by_id(name, accept_missing_image=True) else: repository, tag = utils.parse_repository_tag(name) if not tag: tag = 'latest' self.log('Fetching image %s:%s' % (repository, tag)) image = self.client.find_image(name=repository, tag=tag) if image: results.append(image) return results
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, accept_missing_image=True) 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 NotFound: # If the image vanished while we were trying to remove it, don't fail pass except Exception as exc: self.fail("Error removing image %s - %s" % (name, to_native(exc))) self.results['changed'] = True self.results['actions'].append("Removed image %s" % (name)) self.results['image']['state'] = 'Deleted'
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 ''' if is_image_name_id(name): self.fail("Cannot push an image ID: %s" % name) 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 'unauthorized' in str(exc): if 'authentication required' in str(exc): self.fail( "Error pushing image %s/%s:%s - %s. Try logging into %s first." % (registry, repo_name, tag, to_native(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, to_native(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 load_images(self): ''' Load images from a .tar archive ''' # Load image(s) from file load_output = [] try: self.log("Opening image {0}".format(self.path)) with open(self.path, 'rb') as image_tar: self.log("Loading images from {0}".format(self.path)) for line in self.client.load_image(image_tar): self.log(line, pretty_print=True) self._extract_output_line(line, load_output) except EnvironmentError as exc: if exc.errno == errno.ENOENT: self.client.fail("Error opening archive {0} - {1}".format( self.path, to_native(exc))) self.client.fail("Error loading archive {0} - {1}".format( self.path, to_native(exc)), stdout='\n'.join(load_output)) except Exception as exc: self.client.fail("Error loading archive {0} - {1}".format( self.path, to_native(exc)), stdout='\n'.join(load_output)) # Collect loaded images loaded_images = [] for line in load_output: if line.startswith('Loaded image:'): loaded_images.append(line[len('Loaded image:'):].strip()) if line.startswith('Loaded image ID:'): loaded_images.append(line[len('Loaded image ID:'):].strip()) if not loaded_images: self.client.fail( "Detected no loaded images. Archive potentially corrupt?", stdout='\n'.join(load_output)) images = [] for image_name in loaded_images: if is_image_name_id(image_name): images.append(self.client.find_image_by_id(image_name)) elif ':' in image_name: image_name, tag = image_name.rsplit(':', 1) images.append(self.client.find_image(image_name, tag)) else: self.client.module.warn( 'Image name "{0}" is neither ID nor has a tag'.format( image_name)) self.results['image_names'] = loaded_images self.results['images'] = images self.results['changed'] = True self.results['stdout'] = '\n'.join(load_output)
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() pull = parameters['pull'] or dict() self.archive_path = parameters['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['force_source'] self.force_absent = parameters['force_absent'] self.force_tag = parameters['force_tag'] self.load_path = parameters['load_path'] self.name = parameters['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['repository'] self.rm = build.get('rm', True) self.state = parameters['state'] self.tag = parameters['tag'] self.http_timeout = build.get('http_timeout') self.pull_platform = pull.get('platform') self.push = parameters['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 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" if is_image_name_id(name): image = self.client.find_image_by_id(name, accept_missing_image=True) image_name = name else: image = self.client.find_image(name=name, tag=tag) image_name = "%s:%s" % (name, tag) if not image: self.log("archive image: image %s not found" % image_name) return 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: saved_image = self.client.get_image(image_name) except Exception as exc: self.fail("Error getting image %s - %s" % (image_name, to_native(exc))) try: with open(self.archive_path, 'wb') as fd: if self.client.docker_py_version >= LooseVersion('3.0.0'): for chunk in saved_image: fd.write(chunk) else: for chunk in saved_image.stream(2048, decode_content=False): fd.write(chunk) except Exception as exc: self.fail("Error writing image archive %s - %s" % (self.archive_path, to_native(exc))) if image: self.results['image'] = image
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: image_name = name if not is_image_name_id(name) and tag and not name.endswith(':' + tag): image_name = "%s:%s" % (name, tag) self.log("tagging %s to %s:%s" % (image_name, repo, repo_tag)) self.results['changed'] = True self.results['actions'].append("Tagged image %s to %s:%s" % (image_name, 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. 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" % to_native(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)
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 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, to_native(exc))) self.client.fail("Error loading image %s - %s" % (self.name, to_native(exc)), stdout='\n'.join(load_output)) except Exception as exc: self.client.fail("Error loading image %s - %s" % (self.name, to_native(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() loaded_image_ids = set() for line in load_output: if line.startswith('Loaded image:'): loaded_images.add(line[len('Loaded image:'):].strip()) if line.startswith('Loaded image ID:'): loaded_image_ids.add( line[len('Loaded image ID:'):].strip().lower()) if not loaded_images and not loaded_image_ids: self.client.fail( "Detected no loaded images. Archive potentially corrupt?", stdout='\n'.join(load_output)) if is_image_name_id(self.name): expected_image = self.name.lower() found_image = expected_image not in loaded_image_ids else: expected_image = '%s:%s' % (self.name, self.tag) found_image = expected_image not in loaded_images if found_image: self.client.fail( "The archive did not contain image '%s'. Instead, found %s." % (expected_image, ', '.join( sorted(["'%s'" % image for image in loaded_images] + list(loaded_image_ids)))), 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( sorted(["'%s'" % image for image in loaded_images] + list(loaded_image_ids))), )) if is_image_name_id(self.name): return self.client.find_image_by_id(self.name, accept_missing_image=True) else: return self.client.find_image(self.name, self.tag)
def present(self): ''' Handles state = 'present', which includes building, loading or pulling an image, depending on user provided parameters. :returns None ''' if is_image_name_id(self.name): image = self.client.find_image_by_id(self.name, accept_missing_image=True) else: image = self.client.find_image(name=self.name, tag=self.tag) if not image or self.force_source: if self.source == 'build': if is_image_name_id(self.name): self.fail( "Image name must not be an image ID for source=build; got: %s" % self.name) # 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 and not is_image_name_id(image_name): 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': if is_image_name_id(self.name): self.fail( "Image name must not be an image ID for source=pull; got: %s" % self.name) # 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, platform=self.pull_platform) elif self.source == 'local': if image is None: name = self.name if self.tag and not is_image_name_id(name): 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 else: self.results['image'] = image 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)