Skip to content
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

Trigger a new image build when we detect that the Containerfile has changed. #811

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 94 additions & 3 deletions plugins/modules/podman_image.py
Original file line number Diff line number Diff line change
Expand Up @@ -416,6 +416,7 @@
import shlex # noqa: E402
import tempfile # noqa: E402
import time # noqa: E402
import hashlib # noqa: E402

from ansible.module_utils._text import to_native
from ansible.module_utils.basic import AnsibleModule
Expand Down Expand Up @@ -491,15 +492,102 @@ def _get_id_from_output(self, lines, startswith=None, contains=None, split_on='

return layer_ids[-1]

def _find_containerfile_from_context(self):
"""
Find a Containerfile/Dockerfile path inside a podman build context.
Return 'None' if none exist.
"""

containerfile_path = None
for filename in [os.path.join(self.path, fname) for fname in ["Containerfile", "Dockerfile"]]:
if os.path.exists(filename):
containerfile_path = filename
break
return containerfile_path

def _get_containerfile_contents(self):
"""
Get the path to the Containerfile for an invocation
of the module, and return its contents.

See if either `file` or `container_file` in build args are populated,
fetch their contents if so. If not, return the contents of the Containerfile
or Dockerfile from inside the build context, if present.

If we don't find a Containerfile/Dockerfile in any of the above
locations, return 'None'.
"""

build_file_arg = self.build.get('file') if self.build else None
containerfile_contents = self.build.get('container_file') if self.build else None

container_filename = None
if build_file_arg:
container_filename = build_file_arg
elif self.path and not build_file_arg:
container_filename = self._find_containerfile_from_context()

if not containerfile_contents:
with open(container_filename) as f:
containerfile_contents = f.read()

return containerfile_contents

def _hash_containerfile_contents(self, containerfile_contents):
"""
When given the contents of a Containerfile/Dockerfile,
return a sha256 hash of these contents.
"""
return hashlib.sha256(
containerfile_contents.encode(),
usedforsecurity=False
).hexdigest()

def _get_args_containerfile_hash(self):
"""
If we can find a Containerfile in any of the module args
or inside the build context, hash its contents.

If we don't have this, return an empty string.
"""

args_containerfile_hash = ""

context_has_containerfile = self.path and self._find_containerfile_from_context()

should_hash_args_containerfile = (
context_has_containerfile or
self.build.get('file') is not None or
self.build.get('container_file') is not None
)

if should_hash_args_containerfile:
args_containerfile_hash = self._hash_containerfile_contents(
self._get_containerfile_contents()
)
return args_containerfile_hash
SkrrtBacharach marked this conversation as resolved.
Show resolved Hide resolved

def present(self):
image = self.find_image()

existing_image_containerfile_hash = ""
args_containerfile_hash = self._get_args_containerfile_hash()

if image:
digest_before = image[0].get('Digest', image[0].get('digest'))
labels = image[0].get('Labels') or {}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
labels = image[0].get('Labels') or {}
labels = image[0].get('Labels', {})

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I consistently get a test failure when I make this change

TASK [podman_image : Pull image from docker.io with short url again] ***********
task path: /home/gw/.ansible/collections/ansible_collections/containers/podman/tests/output/.tmp/integration/podman_image-fzafk80g-ÅÑŚÌβŁÈ/tests/integration/targets/podman_image/tasks/main.yml:34
Using module file /home/gw/.ansible/collections/ansible_collections/containers/podman/plugins/modules/podman_image.py
Pipelining is enabled.
<testhost> ESTABLISH LOCAL CONNECTION FOR USER: gw
<testhost> EXEC /bin/sh -c '/home/gw/venvs/molecule-plugins/bin/python3 && sleep 0'
The full traceback is:
Traceback (most recent call last):
  File "<stdin>", line 121, in <module>
  File "<stdin>", line 113, in _ansiballz_main
  File "<stdin>", line 61, in invoke_module
  File "<frozen runpy>", line 226, in run_module
  File "<frozen runpy>", line 98, in _run_module_code
  File "<frozen runpy>", line 88, in _run_code
  File "/tmp/ansible_containers.podman.podman_image_payload_zbm4x3jp/ansible_containers.podman.podman_image_payload.zip/ansible_collections/containers/podman/plugins/modules/podman_image.py", line 1033, in <module>
  File "/tmp/ansible_containers.podman.podman_image_payload_zbm4x3jp/ansible_containers.podman.podman_image_payload.zip/ansible_collections/containers/podman/plugins/modules/podman_image.py", line 1028, in main
  File "/tmp/ansible_containers.podman.podman_image_payload_zbm4x3jp/ansible_containers.podman.podman_image_payload.zip/ansible_collections/containers/podman/plugins/modules/podman_image.py", line 462, in __init__
  File "/tmp/ansible_containers.podman.podman_image_payload_zbm4x3jp/ansible_containers.podman.podman_image_payload.zip/ansible_collections/containers/podman/plugins/modules/podman_image.py", line 579, in present
TypeError: argument of type 'NoneType' is not iterable
fatal: [testhost]: FAILED! => {
    "changed": false,
    "module_stderr": "Traceback (most recent call last):\n  File \"<stdin>\", line 121, in <module>\n  File \"<stdin>\", line 113, in _ansiballz_main\n  File \"<stdin>\", line 61, in invoke_module\n  File \"<frozen runpy>\", line 226, in run_module\n  File \"<frozen runpy>\", line 98, in _run_module_code\n  File \"<frozen runpy>\", line 88, in _run_code\n  File \"/tmp/ansible_containers.podman.podman_image_payload_zbm4x3jp/ansible_containers.podman.podman_image_payload.zip/ansible_collections/containers/podman/plugins/modules/podman_image.py\", line 1033, in <module>\n  File \"/tmp/ansible_containers.podman.podman_image_payload_zbm4x3jp/ansible_containers.podman.podman_image_payload.zip/ansible_collections/containers/podman/plugins/modules/podman_image.py\", line 1028, in main\n  File \"/tmp/ansible_containers.podman.podman_image_payload_zbm4x3jp/ansible_containers.podman.podman_image_payload.zip/ansible_collections/containers/podman/plugins/modules/podman_image.py\", line 462, in __init__\n  File \"/tmp/ansible_containers.podman.podman_image_payload_zbm4x3jp/ansible_containers.podman.podman_image_payload.zip/ansible_collections/containers/podman/plugins/modules/podman_image.py\", line 579, in present\nTypeError: argument of type 'NoneType' is not iterable\n",
    "module_stdout": "",
    "msg": "MODULE FAILURE\nSee stdout/stderr for the exact error",
    "rc": 1
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@sshnaidm I couldn't figure out why this was giving me an error.

if "containerfile.hash" in labels:
existing_image_containerfile_hash = labels["containerfile.hash"]
else:
digest_before = None

if not image or self.force:
both_hashes_exist_and_differ = (
args_containerfile_hash != "" and
existing_image_containerfile_hash != "" and
args_containerfile_hash != existing_image_containerfile_hash
)

if not image or self.force or both_hashes_exist_and_differ:
if self.state == 'build' or self.path:
# Build the image
build_file = self.build.get('file') if self.build else None
Expand All @@ -513,7 +601,7 @@ def present(self):
self.results['actions'].append('Built image {image_name} from {path}'.format(
image_name=self.image_name, path=self.path or 'default context'))
if not self.module.check_mode:
self.results['image'], self.results['stdout'] = self.build_image()
self.results['image'], self.results['stdout'] = self.build_image(args_containerfile_hash)
image = self.results['image']
else:
# Pull the image
Expand Down Expand Up @@ -649,7 +737,7 @@ def pull_image(self, image_name=None):
msg='Failed to pull image {image_name}'.format(image_name=image_name))
return self.inspect_image(out.strip())

def build_image(self):
def build_image(self, containerfile_hash):
args = ['build']
args.extend(['-t', self.image_name])

Expand Down Expand Up @@ -698,6 +786,9 @@ def build_image(self):
f.write(container_file_txt)
args.extend(['--file', container_file_path])

if containerfile_hash:
args.extend(['--label', f"containerfile.hash={containerfile_hash}"])

volume = self.build.get('volume')
if volume:
for v in volume:
Expand Down
30 changes: 30 additions & 0 deletions tests/integration/targets/podman_image/tasks/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,29 @@
register: oci_build6
ignore_errors: true

- name: Build OCI image testimage6 twice with the same Containerfile
containers.podman.podman_image:
executable: "{{ test_executable | default('podman') }}"
name: testimage6
state: build
build:
format: oci
container_file: |-
FROM quay.io/coreos/alpine-sh
register: oci_build7
loop: [0, 1]

- name: Build OCI image testimage6 with a different Containerfile
containers.podman.podman_image:
executable: "{{ test_executable | default('podman') }}"
name: testimage6
state: build
build:
format: oci
container_file: |-
FROM docker.io/alpine
register: oci_build8

- name: Inspect first image
containers.podman.podman_image_info:
executable: "{{ test_executable | default('podman') }}"
Expand All @@ -259,6 +282,13 @@
- oci_build4 is success
- oci_build5 is success
- oci_build6 is failed
# The following line tests that building an image twice with
# the same Containerfile doesn't rebuild the image.
- oci_build7.results[1] is not changed
# oci_build8 tests that building an image with the same name
# but a different Containerfile results in a new image being
# built.
- oci_build8 is changed
- "'localhost/testimage:latest' in testimage_info.images[0]['RepoTags'][0]"
- "'localhost/testimage2:latest' in testimage2_info.images[0]['RepoTags'][0]"
- "'no such file or directory' in oci_build3.msg"
Expand Down