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
40 changes: 34 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,41 @@ 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
if require_version:
systems = self._ez_client.find_systems(name=require_name, **kwargs)
valid_versions = list(
require_version.filter(
[str(Version(system.version)) for system in systems]
)
)
if valid_versions:
system_candidates = [
Copy link
Contributor

Choose a reason for hiding this comment

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

Mapping back and forth from str(Version) to system.version doesn't always match because Version parsing will sometimes fix formatting issues so it is no longer a one to one Mapping. Check out how System Client handles it in _determine_latest

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think this situation is slightly different. I think changing an invalid requires version to a valid package version is ok. If a version can't be parsed at all it should fail. Anyway, I updated tests to show what happens in the various cases. We can debate more.

system for system in systems if 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
else:
system = self._ez_client.find_unique_system(
name=require_name, filter_latest=True, **kwargs
)

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
30 changes: 30 additions & 0 deletions brewtils/test/fixtures.py
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,36 @@ 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 child_request_dict(ts_epoch):
"""A child request represented as a dictionary."""
Expand Down
176 changes: 176 additions & 0 deletions test/plugin_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -960,3 +960,179 @@ 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
def test_no_specifier(
plugin, bg_system, bg_system_2, bg_system_3, bg_system_4, bg_system_5
):
p = Plugin(bg_host="localhost", system=bg_system)
p._ez_client.find_unique_system.return_value = bg_system_5
# Expect 3.0.0 as latest valid version
# Expect 3.0.0
assert p.get_system_dependency("system").version == bg_system_5.version

def test_equals(
plugin, bg_system, bg_system_2, bg_system_3, bg_system_4, bg_system_5
):
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,
]
# Expect 2.1.1 valid
# Expect 2.1.1
assert p.get_system_dependency("system==2.1.0").version == bg_system_3.version

def test_compatible_release(
plugin, bg_system, bg_system_2, bg_system_3, bg_system_4, bg_system_5
):
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,
]
# Expect 2.1.0, 2.1.1 valid
# Expect 2.1.1
assert p.get_system_dependency("system~=2.1.0").version == bg_system_4.version

def test_wildcard_minor(
plugin, bg_system, bg_system_2, bg_system_3, bg_system_4, bg_system_5
):
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,
]
# Expect 1.0.0, 2.0.0, 2.1.1, 3.0.0 valid
# Expect 3.0.0
assert p.get_system_dependency("system==2.*").version == bg_system_4.version

def test_wildcard_patch(
plugin, bg_system, bg_system_2, bg_system_3, bg_system_4, bg_system_5
):
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,
]
# Expect 1.0.0, 2.0.0, 2.1.1, 3.0.0 valid
# Expect 3.0.0
assert p.get_system_dependency("system==2.1.*").version == bg_system_4.version

def test_excludes(
plugin, bg_system, bg_system_2, bg_system_3, bg_system_4, bg_system_5
):
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,
]
# Expect 1.0.0, 2.0.0, 2.1.1, 3.0.0 valid
# Expect 3.0.0
assert p.get_system_dependency("system!=2.1.0").version == bg_system_5.version

def test_gt(plugin, bg_system, bg_system_2, bg_system_3, bg_system_4, bg_system_5):
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,
]
# Expect 2.1.1, 3.0.0 valid
# Expect 3.0.0
assert p.get_system_dependency("system>2.1.0").version == bg_system_5.version

def test_gte(plugin, bg_system, bg_system_2, bg_system_3, bg_system_4, bg_system_5):
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,
]
# Expect 2.1.0, 2.1.1, 3.0.0 valid
# Expect 3.0.0
assert p.get_system_dependency("system>=2.1.0").version == bg_system_5.version

def test_lt(plugin, bg_system, bg_system_2, bg_system_3, bg_system_4, bg_system_5):
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,
]
# Expect 1.0.0, 2.0.0 valid
# Expect 2.0.0
assert p.get_system_dependency("system<2.1.0").version == bg_system_2.version

def test_lte(plugin, bg_system, bg_system_2, bg_system_3, bg_system_4, bg_system_5):
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,
]
# Expect 1.0.0, 2.0.0, 2.1.0 valid
# Expect 2.1.0
assert p.get_system_dependency("system<=2.1.0").version == bg_system_3.version

def test_range(
plugin, bg_system, bg_system_2, bg_system_3, bg_system_4, bg_system_5
):
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,
]
# Expect 1.0.0 valid
# Expect 1.0.0
assert p.get_system_dependency("system<2.0.0,>=1").version == bg_system.version

def test_combo(
plugin, bg_system, bg_system_2, bg_system_3, bg_system_4, bg_system_5
):
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,
]
# Expect 1.0.0 valid
# Expect 1.0.0
assert (
p.get_system_dependency("system==2.*,<2.1.1,!=2.1.0").version
== bg_system_2.version
)
Loading