generated from ansible-collections/collection_template
-
Notifications
You must be signed in to change notification settings - Fork 123
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
docker_image: improve/fix handling of image IDs #87
Merged
felixfontein
merged 9 commits into
ansible-collections:main
from
felixfontein:docker_image-load-ids
Feb 28, 2021
Merged
Changes from 7 commits
Commits
Show all changes
9 commits
Select commit
Hold shift + click to select a range
5b36901
Improve/fix handling of image IDs in docker_image.
felixfontein 0119c77
Fix syntax error.
felixfontein ddef918
Linting.
felixfontein e86863d
Fix name collision.
felixfontein d6ccb70
Add various tests.
felixfontein 4537f8c
Fix tests.
felixfontein 5248b41
Improve image finding by ID, and fix various related bugs.
felixfontein 7f0ec90
accept_not_there -> accept_missing_image.
felixfontein b3b6da6
Remove unnecessary dummy variable.
felixfontein File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
minor_changes: | ||
- "docker_image - properly support image IDs (hashes) for loading and tagging images (https://github.com/ansible-collections/community.docker/issues/86, https://github.com/ansible-collections/community.docker/pull/87)." | ||
bugfixes: | ||
- "docker_image - prevent module failure when removing image that is removed between inspection and removal (https://github.com/ansible-collections/community.docker/pull/87)." | ||
- "docker_image - prevent module failure when removing non-existant image by ID (https://github.com/ansible-collections/community.docker/pull/87)." | ||
- "docker_image_info - prevent module failure when image vanishes between listing and inspection (https://github.com/ansible-collections/community.docker/pull/87)." | ||
- "docker_image_info - prevent module failure when querying non-existant image by ID (https://github.com/ansible-collections/community.docker/pull/87)." |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -155,9 +155,9 @@ | |
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. | ||
- "Image name. Name format will be one of: C(name), C(repository/name), C(registry_server:port/name). | ||
When pushing or pulling an image the name can optionally include the tag by appending C(:tag_name)." | ||
- Note that image IDs (hashes) are only supported for I(state=absent), and for I(state=present) with I(source=load). | ||
type: str | ||
required: yes | ||
push: | ||
|
@@ -329,7 +329,7 @@ | |
else: | ||
from docker.auth.auth import resolve_repository_name | ||
from docker.utils.utils import parse_repository_tag | ||
from docker.errors import DockerException | ||
from docker.errors import DockerException, NotFound | ||
except ImportError: | ||
# missing Docker SDK for Python handled in module_utils.docker.common | ||
pass | ||
|
@@ -380,6 +380,12 @@ def __init__(self, client, results): | |
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': | ||
|
@@ -395,10 +401,16 @@ def present(self): | |
|
||
:returns None | ||
''' | ||
image = self.client.find_image(name=self.name, tag=self.tag) | ||
if is_image_name_id(self.name): | ||
image = self.client.find_image_by_id(self.name, accept_not_there=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) | ||
|
@@ -417,13 +429,16 @@ def present(self): | |
self.fail("Error loading image %s. Specified path %s does not exist." % (self.name, | ||
self.load_path)) | ||
image_name = self.name | ||
if self.tag: | ||
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 | ||
|
@@ -432,11 +447,13 @@ def present(self): | |
elif self.source == 'local': | ||
if image is None: | ||
name = self.name | ||
if self.tag: | ||
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) | ||
|
@@ -454,7 +471,7 @@ def absent(self): | |
''' | ||
name = self.name | ||
if is_image_name_id(name): | ||
image = self.client.find_image_by_id(name) | ||
image = self.client.find_image_by_id(name, accept_not_there=True) | ||
else: | ||
image = self.client.find_image(name, self.tag) | ||
if self.tag: | ||
|
@@ -463,6 +480,9 @@ def absent(self): | |
if not self.check_mode: | ||
try: | ||
self.client.remove_image(name, force=self.force_absent) | ||
except NotFound as dummy: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Since we are not really using it, I'd remove the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed in b3b6da6. |
||
# 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, str(exc))) | ||
|
||
|
@@ -481,33 +501,37 @@ def archive_image(self, name, tag): | |
if not tag: | ||
tag = "latest" | ||
|
||
image = self.client.find_image(name=name, tag=tag) | ||
if is_image_name_id(name): | ||
image = self.client.find_image_by_id(name, accept_not_there=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:%s not found" % (name, tag)) | ||
self.log("archive image: image %s not found" % image_name) | ||
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) | ||
saved_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: | ||
for chunk in saved_image: | ||
fd.write(chunk) | ||
else: | ||
for chunk in image.stream(2048, decode_content=False): | ||
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, str(exc))) | ||
|
||
image = self.client.find_image(name=name, tag=tag) | ||
if image: | ||
self.results['image'] = image | ||
|
||
|
@@ -520,6 +544,9 @@ def push_image(self, name, tag=None): | |
: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) | ||
|
@@ -607,7 +634,7 @@ def _extract_output_line(line, output): | |
# 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) | ||
output.extend(text_line.splitlines()) | ||
|
||
def build_image(self): | ||
''' | ||
|
@@ -728,27 +755,39 @@ def load_image(self): | |
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: | ||
if not loaded_images and not loaded_image_ids: | ||
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: | ||
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(["'%s'" % image for image in sorted(loaded_images)])), | ||
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(["'%s'" % image for image in sorted(loaded_images)]), )) | ||
', '.join(sorted(["'%s'" % image for image in loaded_images] + list(loaded_image_ids))), )) | ||
|
||
return self.client.find_image(self.name, self.tag) | ||
if is_image_name_id(self.name): | ||
return self.client.find_image_by_id(self.name, accept_not_there=True) | ||
else: | ||
return self.client.find_image(self.name, self.tag) | ||
|
||
|
||
def main(): | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I always try to avoid variable names with
not
in them. This line illustrates a bit why: reading it aloud, it goes like "if not accept not there". Maybeaccept_missing_image
?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed in 7f0ec90.