Skip to content

Commit

Permalink
Merge pull request #517 from beer-garden/requires-version
Browse files Browse the repository at this point in the history
Requires version constraints
  • Loading branch information
1maple1 authored Nov 22, 2024
2 parents 1c888fb + 2df9260 commit d46d180
Show file tree
Hide file tree
Showing 4 changed files with 226 additions and 9 deletions.
2 changes: 1 addition & 1 deletion CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@ Brewtils Changelog
------
TBD

- Updated plugin class to accept version contraints for required dependencies. Contraints follow python packaging version specifiers.
- Added new annotation/configuration support for shutdown functions. These functions will be executed at the start
of the shutdown process.
- Added new annotation/configuration support for startup functions. These functions will be executed after `Plugin().run()`
has completed startup processes


3.28.0
------
10/9/24
Expand Down
44 changes: 36 additions & 8 deletions brewtils/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

import appdirs
from box import Box
from packaging.requirements import Requirement
from packaging.version import Version
from requests import ConnectionError as RequestsConnectionError

Expand Down Expand Up @@ -495,17 +496,44 @@ def get_timestamp(add_time: int = None):
return current_timestamp + add_time
return current_timestamp

def get_system_matching_version(self, require, **kwargs):
system = None
req = Requirement(require)
require_name = req.name
require_version = req.specifier
systems = self._ez_client.find_systems(name=require_name, **kwargs)
if require_version:
valid_versions = list(
require_version.filter(
[str(Version(system.version)) for system in systems]
)
)
else:
valid_versions = [str(Version(system.version)) for system in systems]

if valid_versions:
system_candidates = [
system
for system in systems
if str(Version(system.version)) in valid_versions
]
system = system_candidates[0]
for system_candidate in system_candidates:
if Version(system_candidate.version) > Version(system.version):
system = system_candidate

return system

def get_system_dependency(self, require, timeout=300):
wait_time = 0.1
while timeout > 0:
system = self._ez_client.find_unique_system(name=require, local=True)
if (
system
and system.instances
and any("RUNNING" == instance.status for instance in system.instances)
):
system = self.get_system_matching_version(
require, filter_running=True, local=True
)
if system:
self._logger.debug(f"Found system: {system}")
return system
self.logger.error(
self._logger.error(
f"Waiting {wait_time:.1f} seconds before next attempt for {self._system} "
f"dependency for {require}"
)
Expand All @@ -520,7 +548,7 @@ def get_system_dependency(self, require, timeout=300):
def await_dependencies(self, requires, config):
for req in requires:
system = self.get_system_dependency(req, config.requires_timeout)
self.logger.debug(
self._logger.debug(
f"Resolved system {system} for {req}: {config.name} {config.instance_name}"
)

Expand Down
40 changes: 40 additions & 0 deletions brewtils/test/fixtures.py
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,46 @@ def bg_system_2(system_dict, bg_instance, bg_command, bg_command_2):
return System(**dict_copy)


@pytest.fixture
def bg_system_3(system_dict, bg_instance, bg_command, bg_command_2):
"""A system with a different version."""
dict_copy = copy.deepcopy(system_dict)
dict_copy["version"] = "2.1.0"
dict_copy["instances"] = [bg_instance]
dict_copy["commands"] = [bg_command, bg_command_2]
return System(**dict_copy)


@pytest.fixture
def bg_system_4(system_dict, bg_instance, bg_command, bg_command_2):
"""A system with a different version."""
dict_copy = copy.deepcopy(system_dict)
dict_copy["version"] = "2.1.1"
dict_copy["instances"] = [bg_instance]
dict_copy["commands"] = [bg_command, bg_command_2]
return System(**dict_copy)


@pytest.fixture
def bg_system_5(system_dict, bg_instance, bg_command, bg_command_2):
"""A system with a different version."""
dict_copy = copy.deepcopy(system_dict)
dict_copy["version"] = "3.0.0"
dict_copy["instances"] = [bg_instance]
dict_copy["commands"] = [bg_command, bg_command_2]
return System(**dict_copy)


@pytest.fixture
def bg_system_6(system_dict, bg_instance, bg_command, bg_command_2):
"""A system with a different version."""
dict_copy = copy.deepcopy(system_dict)
dict_copy["version"] = "3.0.0.dev0"
dict_copy["instances"] = [bg_instance]
dict_copy["commands"] = [bg_command, bg_command_2]
return System(**dict_copy)


@pytest.fixture
def child_request_dict(ts_epoch):
"""A child request represented as a dictionary."""
Expand Down
149 changes: 149 additions & 0 deletions test/plugin_test.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
# -*- coding: utf-8 -*-
import copy
import logging
import logging.config
import os
import warnings
from packaging.requirements import InvalidRequirement
from packaging.version import InvalidVersion

import pytest
from mock import ANY, MagicMock, Mock
Expand Down Expand Up @@ -369,6 +372,7 @@ def test_success(
return_value=(admin_processor, request_processor)
)
plugin._ez_client.find_unique_system = Mock(return_value=bg_system)
plugin._ez_client.find_systems = Mock(return_value=[bg_system])

plugin._startup()
assert admin_processor.startup.called is True
Expand All @@ -390,6 +394,7 @@ def test_success_no_ns(
return_value=(admin_processor, request_processor)
)
plugin._ez_client.find_unique_system = Mock(return_value=bg_system)
plugin._ez_client.find_systems = Mock(return_value=[bg_system])

plugin._startup()
assert admin_processor.startup.called is True
Expand Down Expand Up @@ -960,3 +965,147 @@ def test_remote_plugin(self):
assert "'RemotePlugin'" in str(warning)
assert "'Plugin'" in str(warning)
assert "4.0" in str(warning)


class TestDependencies(object):
# 1.0.0 bg_system
# 2.0.0 bg_system_2
# 2.1.0 bg_system_3
# 2.1.1 bg_system_4
# 3.0.0 bg_system_5
# 3.0.0.dev0 bg_system_6
@pytest.mark.parametrize(
"latest,versions",
[
("1.0.0", ["1.0.0"]),
("2.0.0", ["1.0.0", "2.0.0"]),
("1.2.0", ["1.0.0", "1.2.0"]),
("1.0.0", ["1.0.0", "0.2.1rc1"]),
("1.0.0rc1", ["1.0.0rc1", "0.2.1"]),
("1.0.0rc1", ["1.0.0rc1", "0.2.1rc1"]),
("1.0", ["1.0", "0.2.1"]),
("1.0.0", ["1.0.0rc1", "1.0.0"]),
("3.0.0.dev0", ["3.0.0.dev0", "3.0.0.dev"]),
("3.0.0.dev", ["3.0.0.dev", "2.0.0"]),
],
)
def test_determine_latest(client, bg_system, versions, latest):
p = Plugin(bg_host="localhost", system=bg_system)
system_versions = []
for version in versions:
s = copy.deepcopy(bg_system)
s.version = version
system_versions.append(s)
p._ez_client.find_systems.return_value = system_versions
assert p.get_system_dependency("system").version == latest

@pytest.mark.parametrize(
"latest,versions",
[
("b", ["a", "b"]),
("1.0.0", ["a", "b", "1.0.0"]),
],
)
def test_determine_latest_failures(client, bg_system, versions, latest):
p = Plugin(bg_host="localhost", system=bg_system)
system_versions = []
for version in versions:
s = copy.deepcopy(bg_system)
s.version = version
system_versions.append(s)
p._ez_client.find_systems.return_value = system_versions
with pytest.raises(InvalidVersion):
assert p.get_system_dependency("system").version == latest

@pytest.mark.parametrize(
"version_spec,latest",
[
("system", "3.0.0"), # test no specifier ignores pre-release
("system==3.0.0.dev", "3.0.0.dev0"), # test version parsing
("system==2.1.0", "2.1.0"), # test equals
("system==3", "3.0.0"), # test equals no dev
("system~=2.1.0", "2.1.1"), # test compatible release
("system==2.*", "2.1.1"), # test minor wildcard
("system==2.1.*", "2.1.1"), # test patch wildcard
("system!=2.1.0", "3.0.0"), # test excludes
("system>2.1.0", "3.0.0"), # test greater than
("system>=2.1.0", "3.0.0"), # test greater than or equal
("system<2.1.0", "2.0.0"), # test less than
("system<=2.1.0", "2.1.0"), # test less than or equal
("system<2.0.0,>=1", "1.0.0"), # test range
("system==2.*,<2.1.1,!=2.1.0", "2.0.0"), # test combination
],
)
def test_version_specifier(
plugin,
bg_system,
bg_system_2,
bg_system_3,
bg_system_4,
bg_system_5,
bg_system_6,
version_spec,
latest,
):
p = Plugin(bg_host="localhost", system=bg_system)
p._ez_client.find_systems.return_value = [
bg_system,
bg_system_2,
bg_system_3,
bg_system_4,
bg_system_5,
bg_system_6,
]
assert p.get_system_dependency(version_spec).version == latest

def test_no_match(
plugin,
bg_system,
bg_system_2,
bg_system_3,
bg_system_4,
bg_system_5,
bg_system_6,
):
p = Plugin(bg_host="localhost", system=bg_system)
p._ez_client.find_systems.return_value = [
bg_system,
bg_system_2,
bg_system_3,
bg_system_4,
bg_system_5,
bg_system_6,
]
p._wait = Mock(return_value=None)
with pytest.raises(PluginValidationError):
assert p.get_system_dependency("system==3.0.1.dev0").version

@pytest.mark.parametrize(
"version_spec",
[
"system==*",
"system==a",
"system$$3.0.0",
],
)
def test_invalid_requirement(
plugin,
bg_system,
bg_system_2,
bg_system_3,
bg_system_4,
bg_system_5,
bg_system_6,
version_spec,
):
p = Plugin(bg_host="localhost", system=bg_system)
p._ez_client.find_systems.return_value = [
bg_system,
bg_system_2,
bg_system_3,
bg_system_4,
bg_system_5,
bg_system_6,
]
with pytest.raises(InvalidRequirement):
p.get_system_dependency(version_spec)

0 comments on commit d46d180

Please sign in to comment.