add --offline option to galaxy collection verify (#74040)
* --offline allows in-place verify for installed collections with manifests * manifest hash, collection name, version, and path are now always displayed * test updates
This commit is contained in:
parent
af7f3fc266
commit
a84c1a5669
6 changed files with 181 additions and 93 deletions
2
changelogs/fragments/galaxy_verify_local.yml
Normal file
2
changelogs/fragments/galaxy_verify_local.yml
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
minor_changes:
|
||||||
|
- ansible-galaxy CLI - ``collection verify`` command now supports a ``--offline`` option for local-only verification
|
|
@ -363,6 +363,9 @@ class GalaxyCLI(CLI):
|
||||||
'path/url to a tar.gz collection artifact. This is mutually exclusive with --requirements-file.')
|
'path/url to a tar.gz collection artifact. This is mutually exclusive with --requirements-file.')
|
||||||
verify_parser.add_argument('-i', '--ignore-errors', dest='ignore_errors', action='store_true', default=False,
|
verify_parser.add_argument('-i', '--ignore-errors', dest='ignore_errors', action='store_true', default=False,
|
||||||
help='Ignore errors during verification and continue with the next specified collection.')
|
help='Ignore errors during verification and continue with the next specified collection.')
|
||||||
|
verify_parser.add_argument('--offline', dest='offline', action='store_true', default=False,
|
||||||
|
help='Validate collection integrity locally without contacting server for '
|
||||||
|
'canonical manifest hash.')
|
||||||
verify_parser.add_argument('-r', '--requirements-file', dest='requirements',
|
verify_parser.add_argument('-r', '--requirements-file', dest='requirements',
|
||||||
help='A file containing a list of collections to be verified.')
|
help='A file containing a list of collections to be verified.')
|
||||||
|
|
||||||
|
@ -1078,6 +1081,7 @@ class GalaxyCLI(CLI):
|
||||||
collections = context.CLIARGS['args']
|
collections = context.CLIARGS['args']
|
||||||
search_paths = context.CLIARGS['collections_path']
|
search_paths = context.CLIARGS['collections_path']
|
||||||
ignore_errors = context.CLIARGS['ignore_errors']
|
ignore_errors = context.CLIARGS['ignore_errors']
|
||||||
|
local_verify_only = context.CLIARGS['offline']
|
||||||
requirements_file = context.CLIARGS['requirements']
|
requirements_file = context.CLIARGS['requirements']
|
||||||
|
|
||||||
requirements = self._require_one_of_collections_requirements(
|
requirements = self._require_one_of_collections_requirements(
|
||||||
|
@ -1090,6 +1094,7 @@ class GalaxyCLI(CLI):
|
||||||
verify_collections(
|
verify_collections(
|
||||||
requirements, resolved_paths,
|
requirements, resolved_paths,
|
||||||
self.api_servers, ignore_errors,
|
self.api_servers, ignore_errors,
|
||||||
|
local_verify_only=local_verify_only,
|
||||||
artifacts_manager=artifacts_manager,
|
artifacts_manager=artifacts_manager,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -8,6 +8,7 @@ __metaclass__ = type
|
||||||
|
|
||||||
import errno
|
import errno
|
||||||
import fnmatch
|
import fnmatch
|
||||||
|
import functools
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
|
@ -99,6 +100,7 @@ from ansible.galaxy import get_collections_galaxy_meta_info
|
||||||
from ansible.galaxy.collection.concrete_artifact_manager import (
|
from ansible.galaxy.collection.concrete_artifact_manager import (
|
||||||
_consume_file,
|
_consume_file,
|
||||||
_download_file,
|
_download_file,
|
||||||
|
_get_json_from_installed_dir,
|
||||||
_get_meta_from_src_dir,
|
_get_meta_from_src_dir,
|
||||||
_tarfile_extract,
|
_tarfile_extract,
|
||||||
)
|
)
|
||||||
|
@ -124,6 +126,7 @@ from ansible.utils.version import SemanticVersion
|
||||||
display = Display()
|
display = Display()
|
||||||
|
|
||||||
MANIFEST_FORMAT = 1
|
MANIFEST_FORMAT = 1
|
||||||
|
MANIFEST_FILENAME = 'MANIFEST.json'
|
||||||
|
|
||||||
ModifiedContent = namedtuple('ModifiedContent', ['filename', 'expected', 'installed'])
|
ModifiedContent = namedtuple('ModifiedContent', ['filename', 'expected', 'installed'])
|
||||||
|
|
||||||
|
@ -131,52 +134,69 @@ ModifiedContent = namedtuple('ModifiedContent', ['filename', 'expected', 'instal
|
||||||
def verify_local_collection(
|
def verify_local_collection(
|
||||||
local_collection, remote_collection,
|
local_collection, remote_collection,
|
||||||
artifacts_manager,
|
artifacts_manager,
|
||||||
): # type: (Candidate, Candidate, ConcreteArtifactsManager) -> None
|
): # type: (Candidate, Optional[Candidate], ConcreteArtifactsManager) -> None
|
||||||
"""Verify integrity of the locally installed collection.
|
"""Verify integrity of the locally installed collection.
|
||||||
|
|
||||||
:param local_collection: Collection being checked.
|
:param local_collection: Collection being checked.
|
||||||
:param remote_collection: Correct collection.
|
:param remote_collection: Upstream collection (optional, if None, only verify local artifact)
|
||||||
:param artifacts_manager: Artifacts manager.
|
:param artifacts_manager: Artifacts manager.
|
||||||
"""
|
"""
|
||||||
b_temp_tar_path = ( # NOTE: AnsibleError is raised on URLError
|
|
||||||
artifacts_manager.get_artifact_path
|
|
||||||
if remote_collection.is_concrete_artifact
|
|
||||||
else artifacts_manager.get_galaxy_artifact_path
|
|
||||||
)(remote_collection)
|
|
||||||
|
|
||||||
b_collection_path = to_bytes(
|
b_collection_path = to_bytes(
|
||||||
local_collection.src, errors='surrogate_or_strict',
|
local_collection.src, errors='surrogate_or_strict',
|
||||||
)
|
)
|
||||||
|
|
||||||
display.vvv("Verifying '{coll!s}'.".format(coll=local_collection))
|
display.display("Verifying '{coll!s}'.".format(coll=local_collection))
|
||||||
display.vvv(
|
display.display(
|
||||||
u"Installed collection found at '{path!s}'".
|
u"Installed collection found at '{path!s}'".
|
||||||
format(path=to_text(local_collection.src)),
|
format(path=to_text(local_collection.src)),
|
||||||
)
|
)
|
||||||
display.vvv(
|
|
||||||
u"Remote collection cached as '{path!s}'".
|
|
||||||
format(path=to_text(b_temp_tar_path)),
|
|
||||||
)
|
|
||||||
|
|
||||||
# Compare installed version versus requirement version
|
|
||||||
if local_collection.ver != remote_collection.ver:
|
|
||||||
err = (
|
|
||||||
"{local_fqcn!s} has the version '{local_ver!s}' but "
|
|
||||||
"is being compared to '{remote_ver!s}'".format(
|
|
||||||
local_fqcn=local_collection.fqcn,
|
|
||||||
local_ver=local_collection.ver,
|
|
||||||
remote_ver=remote_collection.ver,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
display.display(err)
|
|
||||||
return
|
|
||||||
|
|
||||||
modified_content = [] # type: List[ModifiedContent]
|
modified_content = [] # type: List[ModifiedContent]
|
||||||
|
|
||||||
# Verify the manifest hash matches before verifying the file manifest
|
verify_local_only = remote_collection is None
|
||||||
expected_hash = _get_tar_file_hash(b_temp_tar_path, 'MANIFEST.json')
|
if verify_local_only:
|
||||||
_verify_file_hash(b_collection_path, 'MANIFEST.json', expected_hash, modified_content)
|
# partial away the local FS detail so we can just ask generically during validation
|
||||||
manifest = _get_json_from_tar_file(b_temp_tar_path, 'MANIFEST.json')
|
get_json_from_validation_source = functools.partial(_get_json_from_installed_dir, b_collection_path)
|
||||||
|
get_hash_from_validation_source = functools.partial(_get_file_hash, b_collection_path)
|
||||||
|
|
||||||
|
# since we're not downloading this, just seed it with the value from disk
|
||||||
|
manifest_hash = get_hash_from_validation_source(MANIFEST_FILENAME)
|
||||||
|
else:
|
||||||
|
# fetch remote
|
||||||
|
b_temp_tar_path = ( # NOTE: AnsibleError is raised on URLError
|
||||||
|
artifacts_manager.get_artifact_path
|
||||||
|
if remote_collection.is_concrete_artifact
|
||||||
|
else artifacts_manager.get_galaxy_artifact_path
|
||||||
|
)(remote_collection)
|
||||||
|
|
||||||
|
display.vvv(
|
||||||
|
u"Remote collection cached as '{path!s}'".format(path=to_text(b_temp_tar_path))
|
||||||
|
)
|
||||||
|
|
||||||
|
# partial away the tarball details so we can just ask generically during validation
|
||||||
|
get_json_from_validation_source = functools.partial(_get_json_from_tar_file, b_temp_tar_path)
|
||||||
|
get_hash_from_validation_source = functools.partial(_get_tar_file_hash, b_temp_tar_path)
|
||||||
|
|
||||||
|
# Compare installed version versus requirement version
|
||||||
|
if local_collection.ver != remote_collection.ver:
|
||||||
|
err = (
|
||||||
|
"{local_fqcn!s} has the version '{local_ver!s}' but "
|
||||||
|
"is being compared to '{remote_ver!s}'".format(
|
||||||
|
local_fqcn=local_collection.fqcn,
|
||||||
|
local_ver=local_collection.ver,
|
||||||
|
remote_ver=remote_collection.ver,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
display.display(err)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Verify the downloaded manifest hash matches the installed copy before verifying the file manifest
|
||||||
|
manifest_hash = get_hash_from_validation_source(MANIFEST_FILENAME)
|
||||||
|
_verify_file_hash(b_collection_path, MANIFEST_FILENAME, manifest_hash, modified_content)
|
||||||
|
|
||||||
|
display.display('MANIFEST.json hash: {manifest_hash}'.format(manifest_hash=manifest_hash))
|
||||||
|
|
||||||
|
manifest = get_json_from_validation_source(MANIFEST_FILENAME)
|
||||||
|
|
||||||
# Use the manifest to verify the file manifest checksum
|
# Use the manifest to verify the file manifest checksum
|
||||||
file_manifest_data = manifest['file_manifest_file']
|
file_manifest_data = manifest['file_manifest_file']
|
||||||
|
@ -185,7 +205,7 @@ def verify_local_collection(
|
||||||
|
|
||||||
# Verify the file manifest before using it to verify individual files
|
# Verify the file manifest before using it to verify individual files
|
||||||
_verify_file_hash(b_collection_path, file_manifest_filename, expected_hash, modified_content)
|
_verify_file_hash(b_collection_path, file_manifest_filename, expected_hash, modified_content)
|
||||||
file_manifest = _get_json_from_tar_file(b_temp_tar_path, file_manifest_filename)
|
file_manifest = get_json_from_validation_source(file_manifest_filename)
|
||||||
|
|
||||||
# Use the file manifest to verify individual file checksums
|
# Use the file manifest to verify individual file checksums
|
||||||
for manifest_data in file_manifest['files']:
|
for manifest_data in file_manifest['files']:
|
||||||
|
@ -199,17 +219,14 @@ def verify_local_collection(
|
||||||
'in the following files:'.
|
'in the following files:'.
|
||||||
format(fqcn=to_text(local_collection.fqcn)),
|
format(fqcn=to_text(local_collection.fqcn)),
|
||||||
)
|
)
|
||||||
display.display(to_text(local_collection.fqcn))
|
|
||||||
display.vvv(to_text(local_collection.src))
|
|
||||||
for content_change in modified_content:
|
for content_change in modified_content:
|
||||||
display.display(' %s' % content_change.filename)
|
display.display(' %s' % content_change.filename)
|
||||||
display.vvv(" Expected: %s\n Found: %s" % (content_change.expected, content_change.installed))
|
display.v(" Expected: %s\n Found: %s" % (content_change.expected, content_change.installed))
|
||||||
# FIXME: Why doesn't this raise a failed return code?
|
|
||||||
else:
|
else:
|
||||||
display.vvv(
|
what = "are internally consistent with its manifest" if verify_local_only else "match the remote collection"
|
||||||
"Successfully verified that checksums for '{coll!s}' "
|
display.display(
|
||||||
'match the remote collection'.
|
"Successfully verified that checksums for '{coll!s}' {what!s}.".
|
||||||
format(coll=local_collection),
|
format(coll=local_collection, what=what),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -297,7 +314,7 @@ def download_collections(
|
||||||
):
|
):
|
||||||
for fqcn, concrete_coll_pin in dep_map.copy().items(): # FIXME: move into the provider
|
for fqcn, concrete_coll_pin in dep_map.copy().items(): # FIXME: move into the provider
|
||||||
if concrete_coll_pin.is_virtual:
|
if concrete_coll_pin.is_virtual:
|
||||||
display.v(
|
display.display(
|
||||||
'Virtual collection {coll!s} is not downloadable'.
|
'Virtual collection {coll!s} is not downloadable'.
|
||||||
format(coll=to_text(concrete_coll_pin)),
|
format(coll=to_text(concrete_coll_pin)),
|
||||||
)
|
)
|
||||||
|
@ -555,6 +572,7 @@ def verify_collections(
|
||||||
search_paths, # type: Iterable[str]
|
search_paths, # type: Iterable[str]
|
||||||
apis, # type: Iterable[GalaxyAPI]
|
apis, # type: Iterable[GalaxyAPI]
|
||||||
ignore_errors, # type: bool
|
ignore_errors, # type: bool
|
||||||
|
local_verify_only, # type: bool
|
||||||
artifacts_manager, # type: ConcreteArtifactsManager
|
artifacts_manager, # type: ConcreteArtifactsManager
|
||||||
): # type: (...) -> None
|
): # type: (...) -> None
|
||||||
r"""Verify the integrity of locally installed collections.
|
r"""Verify the integrity of locally installed collections.
|
||||||
|
@ -563,6 +581,7 @@ def verify_collections(
|
||||||
:param search_paths: Locations for the local collection lookup.
|
:param search_paths: Locations for the local collection lookup.
|
||||||
:param apis: A list of GalaxyAPIs to query when searching for a collection.
|
:param apis: A list of GalaxyAPIs to query when searching for a collection.
|
||||||
:param ignore_errors: Whether to ignore any errors when verifying the collection.
|
:param ignore_errors: Whether to ignore any errors when verifying the collection.
|
||||||
|
:param local_verify_only: When True, skip downloads and only verify local manifests.
|
||||||
:param artifacts_manager: Artifacts manager.
|
:param artifacts_manager: Artifacts manager.
|
||||||
"""
|
"""
|
||||||
api_proxy = MultiGalaxyAPIProxy(apis, artifacts_manager)
|
api_proxy = MultiGalaxyAPIProxy(apis, artifacts_manager)
|
||||||
|
@ -606,33 +625,36 @@ def verify_collections(
|
||||||
else:
|
else:
|
||||||
raise AnsibleError(message=default_err)
|
raise AnsibleError(message=default_err)
|
||||||
|
|
||||||
remote_collection = Candidate(
|
if local_verify_only:
|
||||||
collection.fqcn,
|
remote_collection = None
|
||||||
collection.ver if collection.ver != '*'
|
else:
|
||||||
else local_collection.ver,
|
remote_collection = Candidate(
|
||||||
None, 'galaxy',
|
collection.fqcn,
|
||||||
)
|
collection.ver if collection.ver != '*'
|
||||||
|
else local_collection.ver,
|
||||||
|
None, 'galaxy',
|
||||||
|
)
|
||||||
|
|
||||||
# Download collection on a galaxy server for comparison
|
# Download collection on a galaxy server for comparison
|
||||||
try:
|
try:
|
||||||
# NOTE: Trigger the lookup. If found, it'll cache
|
# NOTE: Trigger the lookup. If found, it'll cache
|
||||||
# NOTE: download URL and token in artifact manager.
|
# NOTE: download URL and token in artifact manager.
|
||||||
api_proxy.get_collection_version_metadata(
|
api_proxy.get_collection_version_metadata(
|
||||||
remote_collection,
|
remote_collection,
|
||||||
)
|
)
|
||||||
except AnsibleError as e: # FIXME: does this actually emit any errors?
|
except AnsibleError as e: # FIXME: does this actually emit any errors?
|
||||||
# FIXME: extract the actual message and adjust this:
|
# FIXME: extract the actual message and adjust this:
|
||||||
expected_error_msg = (
|
expected_error_msg = (
|
||||||
'Failed to find collection {coll.fqcn!s}:{coll.ver!s}'.
|
'Failed to find collection {coll.fqcn!s}:{coll.ver!s}'.
|
||||||
format(coll=collection)
|
|
||||||
)
|
|
||||||
if e.message == expected_error_msg:
|
|
||||||
raise AnsibleError(
|
|
||||||
'Failed to find remote collection '
|
|
||||||
"'{coll!s}' on any of the galaxy servers".
|
|
||||||
format(coll=collection)
|
format(coll=collection)
|
||||||
)
|
)
|
||||||
raise
|
if e.message == expected_error_msg:
|
||||||
|
raise AnsibleError(
|
||||||
|
'Failed to find remote collection '
|
||||||
|
"'{coll!s}' on any of the galaxy servers".
|
||||||
|
format(coll=collection)
|
||||||
|
)
|
||||||
|
raise
|
||||||
|
|
||||||
verify_local_collection(
|
verify_local_collection(
|
||||||
local_collection, remote_collection,
|
local_collection, remote_collection,
|
||||||
|
@ -875,7 +897,7 @@ def _build_collection_tar(
|
||||||
|
|
||||||
with tarfile.open(b_tar_filepath, mode='w:gz') as tar_file:
|
with tarfile.open(b_tar_filepath, mode='w:gz') as tar_file:
|
||||||
# Add the MANIFEST.json and FILES.json file to the archive
|
# Add the MANIFEST.json and FILES.json file to the archive
|
||||||
for name, b in [('MANIFEST.json', collection_manifest_json), ('FILES.json', files_manifest_json)]:
|
for name, b in [(MANIFEST_FILENAME, collection_manifest_json), ('FILES.json', files_manifest_json)]:
|
||||||
b_io = BytesIO(b)
|
b_io = BytesIO(b)
|
||||||
tar_info = tarfile.TarInfo(name)
|
tar_info = tarfile.TarInfo(name)
|
||||||
tar_info.size = len(b)
|
tar_info.size = len(b)
|
||||||
|
@ -941,7 +963,7 @@ def _build_collection_dir(b_collection_path, b_collection_output, collection_man
|
||||||
collection_manifest_json = to_bytes(json.dumps(collection_manifest, indent=True), errors='surrogate_or_strict')
|
collection_manifest_json = to_bytes(json.dumps(collection_manifest, indent=True), errors='surrogate_or_strict')
|
||||||
|
|
||||||
# Write contents to the files
|
# Write contents to the files
|
||||||
for name, b in [('MANIFEST.json', collection_manifest_json), ('FILES.json', files_manifest_json)]:
|
for name, b in [(MANIFEST_FILENAME, collection_manifest_json), ('FILES.json', files_manifest_json)]:
|
||||||
b_path = os.path.join(b_collection_output, to_bytes(name, errors='surrogate_or_strict'))
|
b_path = os.path.join(b_collection_output, to_bytes(name, errors='surrogate_or_strict'))
|
||||||
with open(b_path, 'wb') as file_obj, BytesIO(b) as b_io:
|
with open(b_path, 'wb') as file_obj, BytesIO(b) as b_io:
|
||||||
shutil.copyfileobj(b_io, file_obj)
|
shutil.copyfileobj(b_io, file_obj)
|
||||||
|
@ -1056,7 +1078,7 @@ def install_artifact(b_coll_targz_path, b_collection_path, b_temp_path):
|
||||||
with _tarfile_extract(collection_tar, files_member_obj) as (dummy, files_obj):
|
with _tarfile_extract(collection_tar, files_member_obj) as (dummy, files_obj):
|
||||||
files = json.loads(to_text(files_obj.read(), errors='surrogate_or_strict'))
|
files = json.loads(to_text(files_obj.read(), errors='surrogate_or_strict'))
|
||||||
|
|
||||||
_extract_tar_file(collection_tar, 'MANIFEST.json', b_collection_path, b_temp_path)
|
_extract_tar_file(collection_tar, MANIFEST_FILENAME, b_collection_path, b_temp_path)
|
||||||
_extract_tar_file(collection_tar, 'FILES.json', b_collection_path, b_temp_path)
|
_extract_tar_file(collection_tar, 'FILES.json', b_collection_path, b_temp_path)
|
||||||
|
|
||||||
for file_info in files['files']:
|
for file_info in files['files']:
|
||||||
|
@ -1243,6 +1265,12 @@ def _get_tar_file_hash(b_path, filename):
|
||||||
return _consume_file(tar_obj)
|
return _consume_file(tar_obj)
|
||||||
|
|
||||||
|
|
||||||
|
def _get_file_hash(b_path, filename): # type: (bytes, str) -> str
|
||||||
|
filepath = os.path.join(b_path, to_bytes(filename, errors='surrogate_or_strict'))
|
||||||
|
with open(filepath, 'rb') as fp:
|
||||||
|
return _consume_file(fp)
|
||||||
|
|
||||||
|
|
||||||
def _is_child_path(path, parent_path, link_name=None):
|
def _is_child_path(path, parent_path, link_name=None):
|
||||||
""" Checks that path is a path within the parent_path specified. """
|
""" Checks that path is a path within the parent_path specified. """
|
||||||
b_path = to_bytes(path, errors='surrogate_or_strict')
|
b_path = to_bytes(path, errors='surrogate_or_strict')
|
||||||
|
|
|
@ -49,6 +49,8 @@ import yaml
|
||||||
|
|
||||||
display = Display()
|
display = Display()
|
||||||
|
|
||||||
|
MANIFEST_FILENAME = 'MANIFEST.json'
|
||||||
|
|
||||||
|
|
||||||
class ConcreteArtifactsManager:
|
class ConcreteArtifactsManager:
|
||||||
"""Manager for on-disk collection artifacts.
|
"""Manager for on-disk collection artifacts.
|
||||||
|
@ -539,26 +541,26 @@ def _get_meta_from_src_dir(
|
||||||
return _normalize_galaxy_yml_manifest(manifest, galaxy_yml)
|
return _normalize_galaxy_yml_manifest(manifest, galaxy_yml)
|
||||||
|
|
||||||
|
|
||||||
def _get_meta_from_installed_dir(
|
def _get_json_from_installed_dir(
|
||||||
b_path, # type: bytes
|
b_path, # type: bytes
|
||||||
): # type: (...) -> Dict[str, Optional[Union[str, List[str], Dict[str, str]]]]
|
filename, # type: str
|
||||||
n_manifest_json = 'MANIFEST.json'
|
): # type: (...) -> Dict
|
||||||
b_manifest_json = to_bytes(n_manifest_json)
|
|
||||||
b_manifest_json_path = os.path.join(b_path, b_manifest_json)
|
b_json_filepath = os.path.join(b_path, to_bytes(filename, errors='surrogate_or_strict'))
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with open(b_manifest_json_path, 'rb') as manifest_fd:
|
with open(b_json_filepath, 'rb') as manifest_fd:
|
||||||
b_manifest_txt = manifest_fd.read()
|
b_json_text = manifest_fd.read()
|
||||||
except (IOError, OSError):
|
except (IOError, OSError):
|
||||||
raise LookupError(
|
raise LookupError(
|
||||||
"The collection {manifest!s} path '{path!s}' does not exist.".
|
"The collection {manifest!s} path '{path!s}' does not exist.".
|
||||||
format(
|
format(
|
||||||
manifest=n_manifest_json,
|
manifest=filename,
|
||||||
path=to_native(b_manifest_json_path),
|
path=to_native(b_json_filepath),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
manifest_txt = to_text(b_manifest_txt, errors='surrogate_or_strict')
|
manifest_txt = to_text(b_json_text, errors='surrogate_or_strict')
|
||||||
|
|
||||||
try:
|
try:
|
||||||
manifest = json.loads(manifest_txt)
|
manifest = json.loads(manifest_txt)
|
||||||
|
@ -566,18 +568,26 @@ def _get_meta_from_installed_dir(
|
||||||
raise AnsibleError(
|
raise AnsibleError(
|
||||||
'Collection tar file member {member!s} does not '
|
'Collection tar file member {member!s} does not '
|
||||||
'contain a valid json string.'.
|
'contain a valid json string.'.
|
||||||
format(member=n_manifest_json),
|
format(member=filename),
|
||||||
)
|
)
|
||||||
else:
|
|
||||||
collection_info = manifest['collection_info']
|
return manifest
|
||||||
|
|
||||||
|
|
||||||
|
def _get_meta_from_installed_dir(
|
||||||
|
b_path, # type: bytes
|
||||||
|
): # type: (...) -> Dict[str, Optional[Union[str, List[str], Dict[str, str]]]]
|
||||||
|
manifest = _get_json_from_installed_dir(b_path, MANIFEST_FILENAME)
|
||||||
|
collection_info = manifest['collection_info']
|
||||||
|
|
||||||
version = collection_info.get('version')
|
version = collection_info.get('version')
|
||||||
if not version:
|
if not version:
|
||||||
raise AnsibleError(
|
raise AnsibleError(
|
||||||
u'Collection metadata file at `{meta_file!s}` is expected '
|
u'Collection metadata file `{manifest_filename!s}` at `{meta_file!s}` is expected '
|
||||||
u'to have a valid SemVer version value but got {version!s}'.
|
u'to have a valid SemVer version value but got {version!s}'.
|
||||||
format(
|
format(
|
||||||
meta_file=to_text(b_manifest_json_path),
|
manifest_filename=MANIFEST_FILENAME,
|
||||||
|
meta_file=to_text(b_path),
|
||||||
version=to_text(repr(version)),
|
version=to_text(repr(version)),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
@ -594,18 +604,16 @@ def _get_meta_from_tar(
|
||||||
format(path=to_native(b_path)),
|
format(path=to_native(b_path)),
|
||||||
)
|
)
|
||||||
|
|
||||||
n_manifest_json = 'MANIFEST.json'
|
|
||||||
|
|
||||||
with tarfile.open(b_path, mode='r') as collection_tar: # type: tarfile.TarFile
|
with tarfile.open(b_path, mode='r') as collection_tar: # type: tarfile.TarFile
|
||||||
try:
|
try:
|
||||||
member = collection_tar.getmember(n_manifest_json)
|
member = collection_tar.getmember(MANIFEST_FILENAME)
|
||||||
except KeyError:
|
except KeyError:
|
||||||
raise AnsibleError(
|
raise AnsibleError(
|
||||||
"Collection at '{path!s}' does not contain the "
|
"Collection at '{path!s}' does not contain the "
|
||||||
'required file {manifest_file!s}.'.
|
'required file {manifest_file!s}.'.
|
||||||
format(
|
format(
|
||||||
path=to_native(b_path),
|
path=to_native(b_path),
|
||||||
manifest_file=n_manifest_json,
|
manifest_file=MANIFEST_FILENAME,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -613,7 +621,7 @@ def _get_meta_from_tar(
|
||||||
if member_obj is None:
|
if member_obj is None:
|
||||||
raise AnsibleError(
|
raise AnsibleError(
|
||||||
'Collection tar file does not contain '
|
'Collection tar file does not contain '
|
||||||
'member {member!s}'.format(member=n_manifest_json),
|
'member {member!s}'.format(member=MANIFEST_FILENAME),
|
||||||
)
|
)
|
||||||
|
|
||||||
text_content = to_text(
|
text_content = to_text(
|
||||||
|
@ -627,7 +635,7 @@ def _get_meta_from_tar(
|
||||||
raise AnsibleError(
|
raise AnsibleError(
|
||||||
'Collection tar file member {member!s} does not '
|
'Collection tar file member {member!s} does not '
|
||||||
'contain a valid json string.'.
|
'contain a valid json string.'.
|
||||||
format(member=n_manifest_json),
|
format(member=MANIFEST_FILENAME),
|
||||||
)
|
)
|
||||||
return manifest['collection_info']
|
return manifest['collection_info']
|
||||||
|
|
||||||
|
|
|
@ -150,7 +150,7 @@
|
||||||
|
|
||||||
- assert:
|
- assert:
|
||||||
that:
|
that:
|
||||||
- "'Collection ansible_test.verify contains modified content in the following files:\nansible_test.verify\n plugins/modules/test_module.py' in verify.stdout"
|
- "'Collection ansible_test.verify contains modified content in the following files:\n plugins/modules/test_module.py' in verify.stdout"
|
||||||
|
|
||||||
- name: modify the FILES.json to match the new checksum
|
- name: modify the FILES.json to match the new checksum
|
||||||
lineinfile:
|
lineinfile:
|
||||||
|
@ -168,7 +168,7 @@
|
||||||
|
|
||||||
- assert:
|
- assert:
|
||||||
that:
|
that:
|
||||||
- "'Collection ansible_test.verify contains modified content in the following files:\nansible_test.verify\n FILES.json' in verify.stdout"
|
- "'Collection ansible_test.verify contains modified content in the following files:\n FILES.json' in verify.stdout"
|
||||||
|
|
||||||
- name: get the checksum of the FILES.json
|
- name: get the checksum of the FILES.json
|
||||||
stat:
|
stat:
|
||||||
|
@ -190,7 +190,7 @@
|
||||||
|
|
||||||
- assert:
|
- assert:
|
||||||
that:
|
that:
|
||||||
- "'Collection ansible_test.verify contains modified content in the following files:\nansible_test.verify\n MANIFEST.json' in verify.stdout"
|
- "'Collection ansible_test.verify contains modified content in the following files:\n MANIFEST.json' in verify.stdout"
|
||||||
|
|
||||||
- name: remove the artifact metadata to test verifying a collection without it
|
- name: remove the artifact metadata to test verifying a collection without it
|
||||||
file:
|
file:
|
||||||
|
@ -221,3 +221,48 @@
|
||||||
that:
|
that:
|
||||||
- verify.failed
|
- verify.failed
|
||||||
- "'Collection ansible_test.verify does not have a MANIFEST.json' in verify.stderr"
|
- "'Collection ansible_test.verify does not have a MANIFEST.json' in verify.stderr"
|
||||||
|
|
||||||
|
- name: update the collection version to something not present on the server
|
||||||
|
lineinfile:
|
||||||
|
regexp: "version: .*"
|
||||||
|
line: "version: '3.0.0'"
|
||||||
|
path: '{{ galaxy_dir }}/scratch/ansible_test/verify/galaxy.yml'
|
||||||
|
|
||||||
|
- name: build the new version
|
||||||
|
command: ansible-galaxy collection build scratch/ansible_test/verify
|
||||||
|
args:
|
||||||
|
chdir: '{{ galaxy_dir }}'
|
||||||
|
|
||||||
|
- name: force-install from local artifact
|
||||||
|
command: ansible-galaxy collection install '{{ galaxy_dir }}/ansible_test-verify-3.0.0.tar.gz' --force
|
||||||
|
environment:
|
||||||
|
ANSIBLE_COLLECTIONS_PATH: '{{ galaxy_dir }}'
|
||||||
|
|
||||||
|
- name: verify locally only, no download or server manifest hash check
|
||||||
|
command: ansible-galaxy collection verify --offline ansible_test.verify
|
||||||
|
environment:
|
||||||
|
ANSIBLE_COLLECTIONS_PATH: '{{ galaxy_dir }}'
|
||||||
|
register: verify
|
||||||
|
|
||||||
|
- assert:
|
||||||
|
that:
|
||||||
|
- >-
|
||||||
|
"Verifying 'ansible_test.verify:3.0.0'." in verify.stdout
|
||||||
|
- '"MANIFEST.json hash: " in verify.stdout'
|
||||||
|
- >-
|
||||||
|
"Successfully verified that checksums for 'ansible_test.verify:3.0.0' are internally consistent with its manifest." in verify.stdout
|
||||||
|
|
||||||
|
- name: append a newline to a module to modify the checksum
|
||||||
|
shell: "echo '' >> {{ module_path }}"
|
||||||
|
|
||||||
|
- name: verify modified collection locally-only (should fail)
|
||||||
|
command: ansible-galaxy collection verify --offline ansible_test.verify
|
||||||
|
register: verify
|
||||||
|
environment:
|
||||||
|
ANSIBLE_COLLECTIONS_PATH: '{{ galaxy_dir }}'
|
||||||
|
ignore_errors: yes
|
||||||
|
|
||||||
|
- assert:
|
||||||
|
that:
|
||||||
|
- "'Collection ansible_test.verify contains modified content in the following files:' in verify.stdout"
|
||||||
|
- "'plugins/modules/test_module.py' in verify.stdout"
|
||||||
|
|
|
@ -243,7 +243,7 @@ def test_build_artifact_from_path_no_version(collection_artifact, monkeypatch):
|
||||||
concrete_artifact_cm = collection.concrete_artifact_manager.ConcreteArtifactsManager(tmp_path, validate_certs=False)
|
concrete_artifact_cm = collection.concrete_artifact_manager.ConcreteArtifactsManager(tmp_path, validate_certs=False)
|
||||||
|
|
||||||
expected = (
|
expected = (
|
||||||
'^Collection metadata file at `.*` is expected to have a valid SemVer '
|
'^Collection metadata file `.*` at `.*` is expected to have a valid SemVer '
|
||||||
'version value but got {empty_unicode_string!r}$'.
|
'version value but got {empty_unicode_string!r}$'.
|
||||||
format(empty_unicode_string=u'')
|
format(empty_unicode_string=u'')
|
||||||
)
|
)
|
||||||
|
|
Loading…
Reference in a new issue