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

Make image archive/save idempotent, using image id and repo tags as keys #500

Conversation

iamjpotts
Copy link
Contributor

@iamjpotts iamjpotts commented Nov 22, 2022

SUMMARY

The docker_image component supports saving images to a tar file, similar to docker image save command. When a docker_image archive task is run repeatedly, it recreates the file every time.

After this change, the task will compare the Docker image ID of the archive file (if any) vs the image stored in the Docker Engine, and if they are the same, the archive file is left untouched.

ISSUE TYPE
  • Feature Pull Request
COMPONENT NAME

docker_image used with archive_path

ADDITIONAL INFORMATION

Consider a scenario with two servers (localhost and target), where an Ansible playbook synchronizes images to a target server.

  1. Playbook run against localhost uses docker_load with archive_path to export an image to a tar
  2. Playbook run using ansible.builtin.file synchronizes the directory with the tar file on localhost with a target server directory
  3. Playbook run using docker_load imports tar file on target server to target server's docker engine

Currently, this workflow regenerates the tar files over and over again, resending them to the target server each time.

After this change, Step 1 only exports the tar files once, until the source image changes, preventing the network copy from localhost to target on subsequent runs.

Export playbook

- hosts: localhost
  connection: local
  vars:
    docker_images:
      - nginx:1.20

  tasks:

    - name: Docker image export directory 
      ansible.builtin.file:
        path: ~/.docker-image-cache
        state: directory
        mode: '0755'

    - name: Export Docker Images
      community.docker.docker_image:
        name: nginx:1.20
        source: local
        archive_path: ~/.docker-image-cache/nginx-1.20.tar
        state: present

Import playbook


- hosts: target-server
  tasks:

    - name: Sync cache directory
      ansible.builtin.copy:
        src: ~/.docker-image-cache
        dest: ~/

    - name: Import Docker Images
      community.docker.docker_image:
        name: nginx:1.20
        source: load
        load_path: ~/.docker-image-cache/nginx-1.20.tar
        state: present

Output, without this PR, run 1 of 2.

PLAY [localhost] **************************************************************************************************************************************************************************************************************************************************************

TASK [Gathering Facts] ********************************************************************************************************************************************************************************************************************************************************
ok: [localhost]

TASK [Docker image export directory] ******************************************************************************************************************************************************************************************************************************************
ok: [localhost]

TASK [Export Docker Images] ***************************************************************************************************************************************************************************************************************************************************
changed: [localhost]

PLAY RECAP ********************************************************************************************************************************************************************************************************************************************************************
localhost                  : ok=3    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   

Output, without this PR, run 2 of 2. Shows changed again


PLAY [localhost] **************************************************************************************************************************************************************************************************************************************************************

TASK [Gathering Facts] ********************************************************************************************************************************************************************************************************************************************************
ok: [localhost]

TASK [Docker image export directory] ******************************************************************************************************************************************************************************************************************************************
ok: [localhost]

TASK [Export Docker Images] ***************************************************************************************************************************************************************************************************************************************************
changed: [localhost]

PLAY RECAP ********************************************************************************************************************************************************************************************************************************************************************
localhost                  : ok=3    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0 

Output, with this PR, run 1 of 2.

PLAY [localhost] **************************************************************************************************************************************************************************************************************************************************************

TASK [Gathering Facts] ********************************************************************************************************************************************************************************************************************************************************
ok: [localhost]

TASK [Docker image export directory] ******************************************************************************************************************************************************************************************************************************************
ok: [localhost]

TASK [Export Docker Images] ***************************************************************************************************************************************************************************************************************************************************
changed: [localhost]

PLAY RECAP ********************************************************************************************************************************************************************************************************************************************************************
localhost                  : ok=3    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   

Output, with this PR, run 2 of 2. Shows ok on second run.


PLAY [localhost] **************************************************************************************************************************************************************************************************************************************************************

TASK [Gathering Facts] ********************************************************************************************************************************************************************************************************************************************************
ok: [localhost]

TASK [Docker image export directory] ******************************************************************************************************************************************************************************************************************************************
ok: [localhost]

TASK [Export Docker Images] ***************************************************************************************************************************************************************************************************************************************************
ok: [localhost]

PLAY RECAP ********************************************************************************************************************************************************************************************************************************************************************
localhost                  : ok=3    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0 

@iamjpotts iamjpotts force-pushed the 20221122-idempotent-image-archival branch 2 times, most recently from bdfd152 to 5ba45c8 Compare November 22, 2022 09:14
@felixfontein felixfontein changed the title Do not merge - make image archive/save idempotent [DO NOT MERGE] make image archive/save idempotent Nov 22, 2022
@iamjpotts iamjpotts force-pushed the 20221122-idempotent-image-archival branch 9 times, most recently from 8058322 to 4a73414 Compare November 22, 2022 22:20
@iamjpotts iamjpotts changed the title [DO NOT MERGE] make image archive/save idempotent Make image archive/save idempotent, using image id as key Nov 22, 2022
@iamjpotts
Copy link
Contributor Author

@felixfontein

I've removed the 'do not merge' label.

I attempted to add two .tar files for use with unit tests but they got flagged by the license checker, since they are not text files and can't be checked for a copyright header.

I worked around that by base64-encoding the files and including them as a string literal.

The files were generated by me using docker image save - is this approach suitable for the licensing issue, or should another be considered?

@felixfontein felixfontein added enhancement New feature or request docker-plain plain Docker (no swarm, no compose, no stack) labels Nov 23, 2022
@felixfontein
Copy link
Collaborator

I attempted to add two .tar files for use with unit tests but they got flagged by the license checker, since they are not text files and can't be checked for a copyright header.

I worked around that by base64-encoding the files and including them as a string literal.

The files were generated by me using docker image save - is this approach suitable for the licensing issue, or should another be considered?

I would really avoid adding binary blobs to this repository. How about creating these files during the integration tests (by running docker image save) and then using the freshly generated files?

@iamjpotts
Copy link
Contributor Author

One of the two tar files is very small (hello world is about 3kb). The other one is larger (about 3 mb) but I can find a smaller substitute, for example creating an image from scratch and adding a text file to it (e.g a copyright notice).

Would two small files be adequate? Is there a way to exclude them from release/distribution packaging?

The unit tests are easier and faster to run, and in principle, I think these tests belong there. They test parsing a file, as opposed to interacting with a service.

Regenerating them each time also introduces variability into the test input; it's nice to have the same exact input across all versions of Python being tested.

@felixfontein
Copy link
Collaborator

Would two small files be adequate? Is there a way to exclude them from release/distribution packaging?

I would really avoid adding any kind of binary file, if not absolutely necessary.

The unit tests are easier and faster to run, and in principle, I think these tests belong there. They test parsing a file, as opposed to interacting with a service.

They are faster to run, but they also are a big problem, since you are including copyrighted contents into this repository. Both the hello-world executable and busybox come with copyright and legal conditions attached.

Regenerating them each time also introduces variability into the test input; it's nice to have the same exact input across all versions of Python being tested.

I don't think this will be a problem.

Copy link
Collaborator

@felixfontein felixfontein left a comment

Choose a reason for hiding this comment

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

Please add a changelog fragment. Thanks.

.gitignore Outdated Show resolved Hide resolved
plugins/modules/docker_image.py Outdated Show resolved Hide resolved
plugins/modules/docker_image.py Outdated Show resolved Hide resolved
plugins/modules/docker_image.py Show resolved Hide resolved
plugins/modules/docker_image.py Outdated Show resolved Hide resolved
plugins/modules/docker_image.py Outdated Show resolved Hide resolved
plugins/modules/docker_image.py Outdated Show resolved Hide resolved
plugins/modules/docker_image.py Outdated Show resolved Hide resolved
plugins/modules/docker_image.py Outdated Show resolved Hide resolved
@iamjpotts iamjpotts force-pushed the 20221122-idempotent-image-archival branch 4 times, most recently from 6ff9156 to ca15ddb Compare November 24, 2022 01:44
@iamjpotts
Copy link
Contributor Author

iamjpotts commented Nov 24, 2022

Changes:

  • Added change log fragment
  • Replaced committed base-64 encoded tar files with runtime-generated stubs with same structure
  • Fixed nested exception handling and added tests for causes
  • Replaced action entries with log.debug statements (self.log calls an empty function with pass inside)
  • Replaced __exit__ attribute conditionals with close() calls
  • Removed old_image_id result
  • Moved exception and function to their own new module, module_utils/image_archive.py

changelogs/fragments/500-idempotent-image-archival.yaml Outdated Show resolved Hide resolved
changelogs/fragments/500-idempotent-image-archival.yaml Outdated Show resolved Hide resolved
plugins/modules/docker_image.py Outdated Show resolved Hide resolved
plugins/modules/docker_image.py Outdated Show resolved Hide resolved
plugins/modules/docker_image.py Outdated Show resolved Hide resolved
plugins/modules/docker_image.py Outdated Show resolved Hide resolved
@iamjpotts iamjpotts changed the title Make image archive/save idempotent, using image id as key [DO NOT MERGE] Make image archive/save idempotent, using image id as key Nov 27, 2022
@felixfontein felixfontein marked this pull request as draft November 27, 2022 18:08
@felixfontein
Copy link
Collaborator

You have a very strange commit list, I guess you should probably rebase to get rid of all the stray changes which do not belong into this PR.

@iamjpotts iamjpotts force-pushed the 20221122-idempotent-image-archival branch 4 times, most recently from ffd76b6 to b1b6618 Compare November 29, 2022 14:11
@iamjpotts
Copy link
Contributor Author

iamjpotts commented Nov 29, 2022

Changes:

  • Fixed docstring syntax of existing and new docstrings
  • Added docstring to ImageManager.__init__ to specify type for Docker client wrapper
  • Replaced self.log with self.client.module.debug
  • Declare alternate image name in same file as test tasks

@iamjpotts iamjpotts force-pushed the 20221122-idempotent-image-archival branch from b1b6618 to e40e415 Compare November 29, 2022 14:21
@iamjpotts
Copy link
Contributor Author

Made one more docstring change to tickle the build after a transient network related failure.

Ready for review.

plugins/modules/docker_image.py Outdated Show resolved Hide resolved
plugins/modules/docker_image.py Outdated Show resolved Hide resolved
plugins/plugin_utils/common_api.py Outdated Show resolved Hide resolved
@iamjpotts iamjpotts force-pushed the 20221122-idempotent-image-archival branch from 50ad5eb to fe71743 Compare November 29, 2022 16:33
@iamjpotts iamjpotts force-pushed the 20221122-idempotent-image-archival branch 2 times, most recently from 2402819 to b52cd49 Compare November 29, 2022 23:11
@iamjpotts
Copy link
Contributor Author

I'm now getting build failures which are timeouts waiting for the temporary docker registry to start.

For example

Made a whitespace change to tickle the build and it failed the same way (three times so far).

I don't think this is related to accepting your import change suggestion, but I rebased right away.

Looking at the CI log, I suspect the registry container is failing to stay running (the previous successful task is a start request), but there isn't any log output from the registry container in the CI log to see why.

@felixfontein
Copy link
Collaborator

This might be caused by ansible/ansible#78550. Simply ignore that failure for now.

@felixfontein
Copy link
Collaborator

Now there's a conflict due to the other PR being merged. After rebasing with the latest main branch all tests should also pass.

@iamjpotts iamjpotts force-pushed the 20221122-idempotent-image-archival branch from 1d4782f to 7353fbb Compare November 30, 2022 22:07
@iamjpotts
Copy link
Contributor Author

Now there's a conflict due to the other PR being merged. After rebasing with the latest main branch all tests should also pass.

I'm updating the docstring style in this PR also, it's not committed yet. I'll drop a message here once the build is green and I've looked over the code one last time.

@iamjpotts iamjpotts force-pushed the 20221122-idempotent-image-archival branch from 7353fbb to 85b2a94 Compare November 30, 2022 22:26
@iamjpotts
Copy link
Contributor Author

Build is green.

I did miss one triple-quote to single quote in a test_docker_image.py

If that doesn't concern you, I don't see anything else to change.

@felixfontein felixfontein merged commit 166d485 into ansible-collections:main Nov 30, 2022
@felixfontein
Copy link
Collaborator

@iamjpotts thanks a lot for your contribution!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
docker-plain plain Docker (no swarm, no compose, no stack) enhancement New feature or request
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants