galaxy - preserve mode properly on artifact (#68418)

* galaxy - preserve mode properly on artifact

* Fix py2 encoding issue

* Update lib/ansible/galaxy/collection.py

Co-Authored-By: Matt Clay <matt@mystile.com>

* Use sane defaults instead of sourcing from tarfile

Co-authored-by: Matt Clay <matt@mystile.com>
pull/68452/head
Jordan Borean 5 years ago committed by GitHub
parent a9d2ceafe4
commit 127d54b363
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -0,0 +1,2 @@
bugfixes:
- ansible-galaxy collection - Preserve executable bit on build and preserve mode on install from what tar member is set to - https://github.com/ansible/ansible/issues/68415

@ -9,6 +9,7 @@ import json
import operator import operator
import os import os
import shutil import shutil
import stat
import sys import sys
import tarfile import tarfile
import tempfile import tempfile
@ -925,7 +926,8 @@ def _build_collection_tar(b_collection_path, b_tar_path, collection_manifest, fi
b_src_path = os.path.join(b_collection_path, to_bytes(filename, errors='surrogate_or_strict')) b_src_path = os.path.join(b_collection_path, to_bytes(filename, errors='surrogate_or_strict'))
def reset_stat(tarinfo): def reset_stat(tarinfo):
tarinfo.mode = 0o0755 if tarinfo.isdir() else 0o0644 existing_is_exec = tarinfo.mode & stat.S_IXUSR
tarinfo.mode = 0o0755 if existing_is_exec or tarinfo.isdir() else 0o0644
tarinfo.uid = tarinfo.gid = 0 tarinfo.uid = tarinfo.gid = 0
tarinfo.uname = tarinfo.gname = '' tarinfo.uname = tarinfo.gname = ''
return tarinfo return tarinfo
@ -1085,10 +1087,18 @@ def _extract_tar_file(tar, filename, b_dest, b_temp_path, expected_hash=None):
if not os.path.exists(b_parent_dir): if not os.path.exists(b_parent_dir):
# Seems like Galaxy does not validate if all file entries have a corresponding dir ftype entry. This check # Seems like Galaxy does not validate if all file entries have a corresponding dir ftype entry. This check
# makes sure we create the parent directory even if it wasn't set in the metadata. # makes sure we create the parent directory even if it wasn't set in the metadata.
os.makedirs(b_parent_dir) os.makedirs(b_parent_dir, mode=0o0755)
shutil.move(to_bytes(tmpfile_obj.name, errors='surrogate_or_strict'), b_dest_filepath) shutil.move(to_bytes(tmpfile_obj.name, errors='surrogate_or_strict'), b_dest_filepath)
# Default to rw-r--r-- and only add execute if the tar file has execute.
tar_member = tar.getmember(to_native(filename, errors='surrogate_or_strict'))
new_mode = 0o644
if stat.S_IMODE(tar_member.mode) & stat.S_IXUSR:
new_mode |= 0o0111
os.chmod(b_dest_filepath, new_mode)
def _get_tar_file_member(tar, filename): def _get_tar_file_member(tar, filename):
n_filename = to_native(filename, errors='surrogate_or_strict') n_filename = to_native(filename, errors='surrogate_or_strict')

@ -25,6 +25,7 @@ import json
import os import os
import pytest import pytest
import shutil import shutil
import stat
import tarfile import tarfile
import tempfile import tempfile
import yaml import yaml
@ -579,6 +580,14 @@ def collection_artifact(collection_skeleton, tmp_path_factory):
''' Creates a collection artifact tarball that is ready to be published and installed ''' ''' Creates a collection artifact tarball that is ready to be published and installed '''
output_dir = to_text(tmp_path_factory.mktemp('test-ÅÑŚÌβŁÈ Output')) output_dir = to_text(tmp_path_factory.mktemp('test-ÅÑŚÌβŁÈ Output'))
# Create a file with +x in the collection so we can test the permissions
execute_path = os.path.join(collection_skeleton, 'runme.sh')
with open(execute_path, mode='wb') as fd:
fd.write(b"echo hi")
# S_ISUID should not be present on extraction.
os.chmod(execute_path, os.stat(execute_path).st_mode | stat.S_ISUID | stat.S_IEXEC)
# Because we call GalaxyCLI in collection_skeleton we need to reset the singleton back to None so it uses the new # Because we call GalaxyCLI in collection_skeleton we need to reset the singleton back to None so it uses the new
# args, we reset the original args once it is done. # args, we reset the original args once it is done.
orig_cli_args = co.GlobalCLIArgs._Singleton__instance orig_cli_args = co.GlobalCLIArgs._Singleton__instance
@ -644,8 +653,9 @@ def test_collection_build(collection_artifact):
with tarfile.open(tar_path, mode='r') as tar: with tarfile.open(tar_path, mode='r') as tar:
tar_members = tar.getmembers() tar_members = tar.getmembers()
valid_files = ['MANIFEST.json', 'FILES.json', 'roles', 'docs', 'plugins', 'plugins/README.md', 'README.md'] valid_files = ['MANIFEST.json', 'FILES.json', 'roles', 'docs', 'plugins', 'plugins/README.md', 'README.md',
assert len(tar_members) == 7 'runme.sh']
assert len(tar_members) == len(valid_files)
# Verify the uid and gid is 0 and the correct perms are set # Verify the uid and gid is 0 and the correct perms are set
for member in tar_members: for member in tar_members:
@ -655,7 +665,7 @@ def test_collection_build(collection_artifact):
assert member.gname == '' assert member.gname == ''
assert member.uid == 0 assert member.uid == 0
assert member.uname == '' assert member.uname == ''
if member.isdir(): if member.isdir() or member.name == 'runme.sh':
assert member.mode == 0o0755 assert member.mode == 0o0755
else: else:
assert member.mode == 0o0644 assert member.mode == 0o0644
@ -700,19 +710,20 @@ def test_collection_build(collection_artifact):
finally: finally:
files_file.close() files_file.close()
assert len(files['files']) == 6 assert len(files['files']) == 7
assert files['format'] == 1 assert files['format'] == 1
assert len(files.keys()) == 2 assert len(files.keys()) == 2
valid_files_entries = ['.', 'roles', 'docs', 'plugins', 'plugins/README.md', 'README.md'] valid_files_entries = ['.', 'roles', 'docs', 'plugins', 'plugins/README.md', 'README.md', 'runme.sh']
for file_entry in files['files']: for file_entry in files['files']:
assert file_entry['name'] in valid_files_entries assert file_entry['name'] in valid_files_entries
assert file_entry['format'] == 1 assert file_entry['format'] == 1
if file_entry['name'] == 'plugins/README.md': if file_entry['name'] in ['plugins/README.md', 'runme.sh']:
assert file_entry['ftype'] == 'file' assert file_entry['ftype'] == 'file'
assert file_entry['chksum_type'] == 'sha256' assert file_entry['chksum_type'] == 'sha256'
# Can't test the actual checksum as the html link changes based on the version. # Can't test the actual checksum as the html link changes based on the version or the file contents
# don't matter
assert file_entry['chksum_sha256'] is not None assert file_entry['chksum_sha256'] is not None
elif file_entry['name'] == 'README.md': elif file_entry['name'] == 'README.md':
assert file_entry['ftype'] == 'file' assert file_entry['ftype'] == 'file'

@ -12,6 +12,7 @@ import os
import pytest import pytest
import re import re
import shutil import shutil
import stat
import tarfile import tarfile
import yaml import yaml
@ -139,6 +140,12 @@ def collection_artifact(request, tmp_path_factory):
galaxy_obj.write(to_bytes(yaml.safe_dump(existing_yaml))) galaxy_obj.write(to_bytes(yaml.safe_dump(existing_yaml)))
galaxy_obj.truncate() galaxy_obj.truncate()
# Create a file with +x in the collection so we can test the permissions
execute_path = os.path.join(collection_path, 'runme.sh')
with open(execute_path, mode='wb') as fd:
fd.write(b"echo hi")
os.chmod(execute_path, os.stat(execute_path).st_mode | stat.S_IEXEC)
call_galaxy_cli(['build', collection_path, '--output-path', test_dir]) call_galaxy_cli(['build', collection_path, '--output-path', test_dir])
collection_tar = os.path.join(test_dir, '%s-%s-0.1.0.tar.gz' % (namespace, collection)) collection_tar = os.path.join(test_dir, '%s-%s-0.1.0.tar.gz' % (namespace, collection))
@ -634,7 +641,12 @@ def test_install_collection(collection_artifact, monkeypatch):
actual_files = os.listdir(collection_path) actual_files = os.listdir(collection_path)
actual_files.sort() actual_files.sort()
assert actual_files == [b'FILES.json', b'MANIFEST.json', b'README.md', b'docs', b'playbooks', b'plugins', b'roles'] assert actual_files == [b'FILES.json', b'MANIFEST.json', b'README.md', b'docs', b'playbooks', b'plugins', b'roles',
b'runme.sh']
assert stat.S_IMODE(os.stat(os.path.join(collection_path, b'plugins')).st_mode) == 0o0755
assert stat.S_IMODE(os.stat(os.path.join(collection_path, b'README.md')).st_mode) == 0o0644
assert stat.S_IMODE(os.stat(os.path.join(collection_path, b'runme.sh')).st_mode) == 0o0755
assert mock_display.call_count == 1 assert mock_display.call_count == 1
assert mock_display.mock_calls[0][1][0] == "Installing 'ansible_namespace.collection:0.1.0' to '%s'" \ assert mock_display.mock_calls[0][1][0] == "Installing 'ansible_namespace.collection:0.1.0' to '%s'" \
@ -668,7 +680,8 @@ def test_install_collection_with_download(galaxy_server, collection_artifact, mo
actual_files = os.listdir(collection_path) actual_files = os.listdir(collection_path)
actual_files.sort() actual_files.sort()
assert actual_files == [b'FILES.json', b'MANIFEST.json', b'README.md', b'docs', b'playbooks', b'plugins', b'roles'] assert actual_files == [b'FILES.json', b'MANIFEST.json', b'README.md', b'docs', b'playbooks', b'plugins', b'roles',
b'runme.sh']
assert mock_display.call_count == 1 assert mock_display.call_count == 1
assert mock_display.mock_calls[0][1][0] == "Installing 'ansible_namespace.collection:0.1.0' to '%s'" \ assert mock_display.mock_calls[0][1][0] == "Installing 'ansible_namespace.collection:0.1.0' to '%s'" \
@ -696,7 +709,8 @@ def test_install_collections_from_tar(collection_artifact, monkeypatch):
actual_files = os.listdir(collection_path) actual_files = os.listdir(collection_path)
actual_files.sort() actual_files.sort()
assert actual_files == [b'FILES.json', b'MANIFEST.json', b'README.md', b'docs', b'playbooks', b'plugins', b'roles'] assert actual_files == [b'FILES.json', b'MANIFEST.json', b'README.md', b'docs', b'playbooks', b'plugins', b'roles',
b'runme.sh']
with open(os.path.join(collection_path, b'MANIFEST.json'), 'rb') as manifest_obj: with open(os.path.join(collection_path, b'MANIFEST.json'), 'rb') as manifest_obj:
actual_manifest = json.loads(to_text(manifest_obj.read())) actual_manifest = json.loads(to_text(manifest_obj.read()))
@ -728,7 +742,7 @@ def test_install_collections_existing_without_force(collection_artifact, monkeyp
actual_files = os.listdir(collection_path) actual_files = os.listdir(collection_path)
actual_files.sort() actual_files.sort()
assert actual_files == [b'README.md', b'docs', b'galaxy.yml', b'playbooks', b'plugins', b'roles'] assert actual_files == [b'README.md', b'docs', b'galaxy.yml', b'playbooks', b'plugins', b'roles', b'runme.sh']
# Filter out the progress cursor display calls. # Filter out the progress cursor display calls.
display_msgs = [m[1][0] for m in mock_display.mock_calls if 'newline' not in m[2] and len(m[1]) == 1] display_msgs = [m[1][0] for m in mock_display.mock_calls if 'newline' not in m[2] and len(m[1]) == 1]
@ -759,7 +773,8 @@ def test_install_collection_with_circular_dependency(collection_artifact, monkey
actual_files = os.listdir(collection_path) actual_files = os.listdir(collection_path)
actual_files.sort() actual_files.sort()
assert actual_files == [b'FILES.json', b'MANIFEST.json', b'README.md', b'docs', b'playbooks', b'plugins', b'roles'] assert actual_files == [b'FILES.json', b'MANIFEST.json', b'README.md', b'docs', b'playbooks', b'plugins', b'roles',
b'runme.sh']
with open(os.path.join(collection_path, b'MANIFEST.json'), 'rb') as manifest_obj: with open(os.path.join(collection_path, b'MANIFEST.json'), 'rb') as manifest_obj:
actual_manifest = json.loads(to_text(manifest_obj.read())) actual_manifest = json.loads(to_text(manifest_obj.read()))

Loading…
Cancel
Save