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

Requires version constraints #517

Merged
merged 11 commits into from
Nov 22, 2024
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
39 changes: 33 additions & 6 deletions brewtils/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import logging
import logging.config
import os
import re
import signal
import sys
import threading
Expand All @@ -12,6 +13,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,15 +497,40 @@ 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 system.version in valid_versions
Copy link
Contributor

Choose a reason for hiding this comment

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

We should compare parsed versions to parsed versions:
system for system in systems if str(Version(system.version)) in valid_versions

Copy link
Contributor Author

Choose a reason for hiding this comment

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

complete

]
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(
f"Waiting {wait_time:.1f} seconds before next attempt for {self._system} "
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
147 changes: 147 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,145 @@ 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"]),
],
)
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)
Loading