Skip to content
This repository has been archived by the owner on Mar 9, 2023. It is now read-only.

Commit

Permalink
Add --collections-lock art to a 'collections lock'
Browse files Browse the repository at this point in the history
Add support for 'collection lockfiles'.
A collections lock specifies a set of collections to install
and the version specs to match.

Fixes ansible#173

Add collections_lock and collections_lockfile models.

Add ansible_galaxy.collections_lockfile with a yaml loader
for a collections lockfile.

Update cli and install action to use the collections lockfile.

The contents of  collections lock file is a yaml file, containing a dictionary.
The dictitionary is the same format as the 'dependencies' dict in
galaxy.yml.

ie, The keys are collection labels (the namespace and name
dot separated ala 'alikins.collection_inspect').

The values is a version spec string. ie "*" or "==1.0.0".

Example contents of a collections lock file:

alikins.collection_inspect: "1.0.0"
alikins.collection_ntp: ">0.0.1,!=0.0.2"
  • Loading branch information
alikins committed May 28, 2019
1 parent 077817e commit 7062eea
Show file tree
Hide file tree
Showing 7 changed files with 135 additions and 0 deletions.
32 changes: 32 additions & 0 deletions ansible_galaxy/actions/install.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,17 @@
import pprint

from ansible_galaxy import collection_artifact
from ansible_galaxy import collections_lockfile
from ansible_galaxy import display
from ansible_galaxy import download
from ansible_galaxy import exceptions
from ansible_galaxy import install
from ansible_galaxy import installed_repository_db
from ansible_galaxy import matchers
from ansible_galaxy import repository_spec_parse
from ansible_galaxy import requirements
from ansible_galaxy.fetch import fetch_factory
from ansible_galaxy.models.collections_lock import CollectionsLock
from ansible_galaxy.models.repository_spec import FetchMethods
from ansible_galaxy.models.requirement import Requirement, RequirementOps
from ansible_galaxy.models.requirement_spec import RequirementSpec
Expand Down Expand Up @@ -95,11 +98,27 @@ def install_repositories_matching_repository_specs(galaxy_context,
force_overwrite=force_overwrite)


def load_collections_lockfile(lockfile_path):
try:
log.debug('Opening the collections lockfile %s', lockfile_path)
with open(lockfile_path, 'r') as lffd:
return collections_lockfile.load(lffd)

except EnvironmentError as exc:
log.exception(exc)

msg = 'Error opening the collections lockfile "%s": %s' % (lockfile_path, exc)
log.error(msg)

raise exceptions.GalaxyClientError(msg)


# FIXME: probably pass the point where passing around all the data to methods makes sense
# so probably needs a stateful class here
def install_repository_specs_loop(galaxy_context,
repository_spec_strings=None,
requirements_list=None,
collections_lockfile_path=None,
editable=False,
namespace_override=None,
display_callback=None,
Expand Down Expand Up @@ -152,6 +171,19 @@ def install_repository_specs_loop(galaxy_context,

requirements_list.append(req)

log.debug('collections_lockfile_path: %s', collections_lockfile_path)

if collections_lockfile_path:
# load collections lockfile as if the 'dependencies' dict from a collection_info
collections_lockfile = load_collections_lockfile(collections_lockfile_path)

dependencies_list = requirements.from_dependencies_dict(collections_lockfile.dependencies)

# Create the CollectionsLock for the validators
collections_lock = CollectionsLock(dependencies=dependencies_list)

requirements_list.extend(collections_lock.dependencies)

log.debug('requirements_list: %s', requirements_list)

while True:
Expand Down
26 changes: 26 additions & 0 deletions ansible_galaxy/collections_lockfile.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import logging

import yaml

from ansible_galaxy import exceptions
from ansible_galaxy.models.collections_lockfile import CollectionsLockfile

log = logging.getLogger(__name__)


# TODO: replace with a generic version for cases
# where SomeClass(**dict_from_yaml) works
def load(data_or_file_object):
data_dict = yaml.safe_load(data_or_file_object)

log.debug('data_dict: %s', data_dict)

try:
collections_lockfile = CollectionsLockfile(dependencies=data_dict)
except ValueError:
raise
except Exception as exc:
log.exception(exc)
raise exceptions.GalaxyClientError("Error parsing collections lockfile: %s" % str(exc))

return collections_lockfile
9 changes: 9 additions & 0 deletions ansible_galaxy/models/collections_lock.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@

import attr


@attr.s(frozen=True)
class CollectionsLock(object):
'''The collections "lock" (ie, manifest) used with --colleections-lock'''

dependencies = attr.ib(factory=list, validator=attr.validators.instance_of(list))
11 changes: 11 additions & 0 deletions ansible_galaxy/models/collections_lockfile.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
from ansible_galaxy.utils import attr_utils
import attr


@attr.s(frozen=True)
class CollectionsLockfile(object):
'''Represents the data in a collections lock yaml file.'''

dependencies = attr.ib(factory=dict,
validator=attr.validators.instance_of(dict),
converter=attr_utils.convert_none_to_empty_dict)
3 changes: 3 additions & 0 deletions ansible_galaxy_cli/cli/galaxy.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,8 @@ def set_action(self):
self.parser.set_usage("usage: %prog info [options] repo_name[,version]")
elif self.action == "install":
self.parser.set_usage("usage: %prog install [options] [collection_name(s)[,version] | collection_artifact_file(s)]")
self.parser.add_option('-l', '--collections-lock', dest='collections_lockfile',
help='Install collections defined in a collections.lock file')
self.parser.add_option('-g', '--global', dest='global_install', action='store_true',
help='Install content to the path containing your global or system-wide content. The default is the '
'global_collections_path configured in your mazer.yml file (/usr/share/ansible/content, if not configured)')
Expand Down Expand Up @@ -279,6 +281,7 @@ def execute_install(self):
rc = install.install_repository_specs_loop(galaxy_context,
editable=self.options.editable_install,
repository_spec_strings=requested_spec_strings,
collections_lockfile_path=self.options.collections_lockfile,
namespace_override=self.options.namespace,
display_callback=self.display,
ignore_errors=self.options.ignore_errors,
Expand Down
23 changes: 23 additions & 0 deletions tests/ansible_galaxy/models/test_collections_lock_model.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import logging

import pytest

from ansible_galaxy.models import collections_lock

log = logging.getLogger(__name__)


def test_init():
res = collections_lock.CollectionsLock()

assert isinstance(res, collections_lock.CollectionsLock)
assert isinstance(res.dependencies, list)
assert res.dependencies == []


def test_init_deps_dict():
not_a_list = {'foo': 'bar'}
with pytest.raises(TypeError, match='.*dependencies.*') as exc_info:
collections_lock.CollectionsLock(dependencies=not_a_list)

log.debug('exc_info: %s', exc_info)
31 changes: 31 additions & 0 deletions tests/ansible_galaxy/models/test_collections_lockfile_model.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import logging

import pytest

from ansible_galaxy.models import collections_lockfile

log = logging.getLogger(__name__)


def test_init():
res = collections_lockfile.CollectionsLockfile()

assert isinstance(res, collections_lockfile.CollectionsLockfile)
assert isinstance(res.dependencies, dict)
assert res.dependencies == {}


def test_init_deps_none():
res = collections_lockfile.CollectionsLockfile(dependencies=None)

assert isinstance(res, collections_lockfile.CollectionsLockfile)
assert isinstance(res.dependencies, dict)
assert res.dependencies == {}


def test_init_deps_dict():
not_a_dict = ['foo', 'bar']
with pytest.raises(TypeError, match='.*dependencies.*') as exc_info:
collections_lockfile.CollectionsLockfile(dependencies=not_a_dict)

log.debug('exc_info: %s', exc_info)

0 comments on commit 7062eea

Please sign in to comment.