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

Add support for major/minor protocol version routing #443

Merged
merged 45 commits into from
Apr 17, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
afd177f
Add debug log for loaded module
nrempel Mar 19, 2020
7376110
refactor action menu into version subdir
nrempel Mar 19, 2020
9da907f
Include current minor version and validate
nrempel Mar 19, 2020
be6e506
use absolute import paths
nrempel Mar 19, 2020
ece7149
Add tests and more validation restrictions
nrempel Mar 19, 2020
ed0b676
more tests
nrempel Mar 19, 2020
c16b301
more tests
nrempel Mar 19, 2020
efdbb06
Refactor basicmessage
nrempel Mar 23, 2020
86273d1
update message_types path
nrempel Mar 23, 2020
7cb06ea
Refactor connection protocol
nrempel Mar 23, 2020
aac4936
Refactor references to connections
nrempel Mar 25, 2020
6287168
reformat code
nrempel Mar 25, 2020
8bf446e
Refactoring connections pacakge
nrempel Mar 25, 2020
e1cee1a
Refactor credentials protocol into versioned package
nrempel Mar 25, 2020
9b016ed
Refactor discovery protocol into version
nrempel Mar 25, 2020
b24a1f6
fix line length
nrempel Mar 25, 2020
2b3a750
Refactor introduction protocol into pacakge
nrempel Mar 25, 2020
136e77a
Update register_routes to handle new format
nrempel Mar 26, 2020
64b133c
Merge branch 'master' of https://github.com/hyperledger/aries-cloudag…
nrempel Mar 26, 2020
f51a7ec
Refactor credentials protocol into versioned package
nrempel Mar 26, 2020
93b4686
Refactor problem report into versioned pacakge
nrempel Mar 26, 2020
50df870
Refactor routing plugin into versioned package
nrempel Mar 26, 2020
eb9cadb
Refactor trustping into versioned package
nrempel Mar 26, 2020
f20cf82
Support loading admin route only packages
nrempel Mar 26, 2020
065692a
Load each protocol version as separate plugin
nrempel Mar 26, 2020
9d33f34
Merge branch 'master' of https://github.com/hyperledger/aries-cloudag…
nrempel Mar 29, 2020
b2e4c5a
Update test
nrempel Mar 29, 2020
b08d72c
Add versioned protocol registration
nrempel Mar 30, 2020
46c198f
fix type bug in parsed message type
nrempel Mar 30, 2020
87e3f42
Support minor version greater than current
nrempel Apr 1, 2020
5d57735
Bubble problem up to dispatcher for problem report
nrempel Apr 1, 2020
4182751
Improve error message
nrempel Apr 1, 2020
9574137
fix lint error
nrempel Apr 2, 2020
b960d0b
Update proto registry to still support raw module registration
nrempel Apr 2, 2020
cb56e05
Merge branch 'master' of https://github.com/hyperledger/aries-cloudag…
nrempel Apr 2, 2020
cd937bf
Update tracing test import path
nrempel Apr 2, 2020
9981c42
Add tests
nrempel Apr 2, 2020
ebbc9a2
remove double use of variable
nrempel Apr 2, 2020
aebd8cc
Add docs
nrempel Apr 2, 2020
e887e28
format code
nrempel Apr 15, 2020
9817969
remove credentials and presentations protocols
nrempel Apr 15, 2020
e65ca35
Merge branch 'master' into proto-versions
nrempel Apr 15, 2020
8a32149
Merge branch 'master' into proto-versions
nrempel Apr 16, 2020
e0d5ce4
Merge branch 'master' of https://github.com/hyperledger/aries-cloudag…
nrempel Apr 17, 2020
4a10237
tweaks
nrempel Apr 17, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 7 additions & 4 deletions aries_cloudagent/config/default_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,13 @@
from ..issuer.base import BaseIssuer
from ..holder.base import BaseHolder
from ..verifier.base import BaseVerifier
from ..protocols.actionmenu.base_service import BaseMenuService
from ..protocols.actionmenu.driver_service import DriverMenuService
from ..protocols.introduction.base_service import BaseIntroductionService
from ..protocols.introduction.demo_service import DemoIntroductionService

# FIXME: We shouldn't rely on a hardcoded message version here.
from ..protocols.actionmenu.v1_0.base_service import BaseMenuService
from ..protocols.actionmenu.v1_0.driver_service import DriverMenuService
from ..protocols.introduction.v0_1.base_service import BaseIntroductionService
from ..protocols.introduction.v0_1.demo_service import DemoIntroductionService

from ..storage.base import BaseStorage
from ..storage.provider import StorageProvider
from ..transport.wire_format import BaseWireFormat
Expand Down
15 changes: 7 additions & 8 deletions aries_cloudagent/connections/models/connection_record.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,12 @@
from ...config.injection_context import InjectionContext
from ...messaging.models.base_record import BaseRecord, BaseRecordSchema
from ...messaging.valid import INDY_DID, INDY_RAW_PUBLIC_KEY, UUIDFour
from ...protocols.connections.messages.connection_invitation import ConnectionInvitation
from ...protocols.connections.messages.connection_request import ConnectionRequest

# FIXME: We shouldn't rely on a hardcoded message version here.
from ...protocols.connections.v1_0.messages.connection_invitation import (
ConnectionInvitation,
)
from ...protocols.connections.v1_0.messages.connection_request import ConnectionRequest
from ...storage.base import BaseStorage
from ...storage.record import StorageRecord

Expand All @@ -24,12 +28,7 @@ class Meta:
WEBHOOK_TOPIC = "connections"
LOG_STATE_FLAG = "debug.connections"
CACHE_ENABLED = True
TAG_NAMES = {
"my_did",
"their_did",
"request_id",
"invitation_key",
}
TAG_NAMES = {"my_did", "their_did", "request_id", "invitation_key"}

RECORD_TYPE = "connection"
RECORD_TYPE_INVITATION = "connection_invitation"
Expand Down
9 changes: 7 additions & 2 deletions aries_cloudagent/core/conductor.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,12 @@
from ..config.logging import LoggingConfigurator
from ..config.wallet import wallet_config
from ..messaging.responder import BaseResponder
from ..protocols.connections.manager import ConnectionManager, ConnectionManagerError

# FIXME: We shouldn't rely on a hardcoded message version here.
from ..protocols.connections.v1_0.manager import (
ConnectionManager,
ConnectionManagerError,
)
from ..transport.inbound.manager import InboundTransportManager
from ..transport.inbound.message import InboundMessage
from ..transport.outbound.base import OutboundDeliveryError
Expand Down Expand Up @@ -73,7 +78,7 @@ async def setup(self):

# Register all inbound transports
self.inbound_transport_manager = InboundTransportManager(
context, self.inbound_message_router, self.handle_not_returned,
context, self.inbound_message_router, self.handle_not_returned
)
await self.inbound_transport_manager.setup()

Expand Down
15 changes: 12 additions & 3 deletions aries_cloudagent/core/dispatcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,12 @@
from ..messaging.request_context import RequestContext
from ..messaging.responder import BaseResponder
from ..messaging.util import datetime_now
from ..protocols.connections.manager import ConnectionManager
from ..protocols.problem_report.message import ProblemReport
from .error import ProtocolMinorVersionNotSupported

# FIXME: We shouldn't rely on a hardcoded message version here.
from ..protocols.connections.v1_0.manager import ConnectionManager
from ..protocols.problem_report.v1_0.message import ProblemReport

from ..transport.inbound.message import InboundMessage
from ..transport.outbound.message import OutboundMessage
from ..utils.stats import Collector
Expand Down Expand Up @@ -204,10 +208,15 @@ async def make_message(self, parsed_msg: dict) -> AgentMessage:

registry: ProtocolRegistry = await self.context.inject(ProtocolRegistry)
message_type = parsed_msg.get("@type")

if not message_type:
raise MessageParseError("Message does not contain '@type' parameter")

message_cls = registry.resolve_message_class(message_type)
try:
message_cls = registry.resolve_message_class(message_type)
except ProtocolMinorVersionNotSupported as e:
raise MessageParseError(f"Problem parsing message type. {e}")

if not message_cls:
raise MessageParseError(f"Unrecognized message type {message_type}")

Expand Down
13 changes: 13 additions & 0 deletions aries_cloudagent/core/error.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,16 @@ def message(self) -> str:

class StartupError(BaseError):
"""Error raised when there is a problem starting the conductor."""


class ProtocolDefinitionValidationError(BaseError):
"""Error raised when there is a problem validating a protocol definition."""


class ProtocolMinorVersionNotSupported(BaseError):
"""
Minimum minor version protocol error.

Error raised when protocol support exists
but minimum minor version is higher than in @type parameter.
"""
220 changes: 199 additions & 21 deletions aries_cloudagent/core/plugin_registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
from types import ModuleType
from typing import Sequence

from .error import ProtocolDefinitionValidationError

from ..config.injection_context import InjectionContext
from ..utils.classloader import ClassLoader, ModuleLoadError

Expand All @@ -30,23 +32,145 @@ def plugins(self) -> Sequence[ModuleType]:
"""Accessor for a list of all plugin modules."""
return list(self._plugins.values())

def validate_version(self, version_list, module_name):
"""Validate version dict format."""

is_list = type(version_list) is list

# Must be a list
if not is_list:
raise ProtocolDefinitionValidationError(
"Versions definition is not of type list"
)

# Must have at least one definition
if len(version_list) < 1:
raise ProtocolDefinitionValidationError(
"Versions list must define at least one version module"
)

for version_dict in version_list:
# Dicts must have correct format
is_dict = type(version_dict) is dict
if not is_dict:
raise ProtocolDefinitionValidationError(
"Element of versions definition list is not of type dict"
)

try:
type(version_dict["major_version"]) is int and type(
version_dict["minimum_minor_version"]
) is int and type(
version_dict["current_minor_version"]
) is int and type(
version_dict["path"]
) is str
except KeyError as e:
raise ProtocolDefinitionValidationError(
f"Element of versions definition list is missing an attribute: {e}"
)

# Version number cannot be negative
if (
version_dict["major_version"] < 0
or version_dict["minimum_minor_version"] < 0
or version_dict["current_minor_version"] < 0
):
raise ProtocolDefinitionValidationError(
"Version number cannot be negative"
)

# Minimum minor version cannot be great than current version
if (
version_dict["minimum_minor_version"]
> version_dict["current_minor_version"]
):
raise ProtocolDefinitionValidationError(
"Minimum supported minor version cannot"
+ " be greater than current minor version"
)

# There can only be one definition per major version
major_version = version_dict["major_version"]
count = 0
for version_dict_outer in version_list:
if version_dict_outer["major_version"] == major_version:
count += 1
if count > 1:
raise ProtocolDefinitionValidationError(
"There can only be one definition per major version. "
+ f"Found {count} for major version {major_version}."
)

# Specified module must be loadable
version_path = version_dict["path"]
mod = ClassLoader.load_module(version_path, module_name)

if not mod:
raise ProtocolDefinitionValidationError(
"Version module path is not "
+ f"loadable: {module_name}, {version_path}"
)

return True

def register_plugin(self, module_name: str) -> ModuleType:
"""Register a plugin module."""
if module_name in self._plugins:
mod = self._plugins[module_name]
else:
try:
mod = ClassLoader.load_module(module_name)
LOGGER.debug(f"Loaded module: {module_name}")
except ModuleLoadError as e:
LOGGER.error("Error loading plugin module: %s", e)
mod = None
else:
if mod:
self._plugins[module_name] = mod
else:
LOGGER.error("Plugin module not found: %s", module_name)
LOGGER.error(f"Error loading plugin module: {e}")
return None

# Module must exist
if not mod:
LOGGER.error(f"Module doesn't exist: {module_name}")
return None

# Make an exception for non-protocol modules
# that contain admin routes and for old-style protocol
# modules with out version support
routes = ClassLoader.load_module("routes", module_name)
message_types = ClassLoader.load_module("message_types", module_name)
if routes or message_types:
self._plugins[module_name] = mod
return mod

definition = ClassLoader.load_module("definition", module_name)

# definition.py must exist in protocol
if not definition:
LOGGER.error(f"Protocol does not include definition.py: {module_name}")
return None

# definition.py must include versions attribute
if not hasattr(definition, "versions"):
LOGGER.error(
"Protocol definition does not "
+ f"include versions attribute: {module_name}"
)
return None

# Definition list must not be malformed
try:
self.validate_version(definition.versions, module_name)
except ProtocolDefinitionValidationError as e:
LOGGER.error(f"Protocol versions definition is malformed. {e}")
return None

self._plugins[module_name] = mod
return mod

# # Load each version as a separate plugin
# for version in definition.versions:
# mod = ClassLoader.load_module(f"{module_name}.{version['path']}")
# self._plugins[module_name] = mod
# return mod

def register_package(self, package_name: str) -> Sequence[ModuleType]:
"""Register all modules (sub-packages) under a given package name."""
try:
Expand All @@ -67,32 +191,86 @@ async def init_context(self, context: InjectionContext):
if hasattr(plugin, "setup"):
await plugin.setup(context)
else:
await self.load_message_types(context, plugin)
await self.load_protocols(context, plugin)

async def load_message_types(self, context: InjectionContext, plugin: ModuleType):
"""For modules that don't implement setup, register protocols manually."""
async def load_protocol_version(
self,
context: InjectionContext,
mod: ModuleType,
version_definition: dict = None,
):
"""Load a particular protocol version."""
registry = await context.inject(ProtocolRegistry)

if hasattr(mod, "MESSAGE_TYPES"):
registry.register_message_types(
mod.MESSAGE_TYPES, version_definition=version_definition
)
if hasattr(mod, "CONTROLLERS"):
registry.register_controllers(
mod.CONTROLLERS, version_definition=version_definition
)

async def load_protocols(self, context: InjectionContext, plugin: ModuleType):
"""For modules that don't implement setup, register protocols manually."""

# If this module contains message_types, then assume that
# this is a valid module of the old style (not versioned)
try:
mod = ClassLoader.load_module(plugin.__name__ + ".message_types")
except ModuleLoadError as e:
LOGGER.error("Error loading plugin module message types: %s", e)
return

if mod:
if hasattr(mod, "MESSAGE_TYPES"):
registry.register_message_types(mod.MESSAGE_TYPES)
if hasattr(mod, "CONTROLLERS"):
registry.register_controllers(mod.CONTROLLERS)
await self.load_protocol_version(context, mod)
else:
# Otherwise, try check for definition.py for versioned
# protocol packages
try:
definition = ClassLoader.load_module(plugin.__name__ + ".definition")
except ModuleLoadError as e:
LOGGER.error("Error loading plugin definition module: %s", e)
return

if definition:
for protocol_version in definition.versions:
try:
mod = ClassLoader.load_module(
f"{plugin.__name__}.{protocol_version['path']}"
+ ".message_types"
)
await self.load_protocol_version(context, mod, protocol_version)

except ModuleLoadError as e:
LOGGER.error("Error loading plugin module message types: %s", e)
return

async def register_admin_routes(self, app):
"""Call route registration methods on the current context."""
for plugin in self._plugins.values():
try:
mod = ClassLoader.load_module(plugin.__name__ + ".routes")
except ModuleLoadError as e:
LOGGER.error("Error loading admin routes: %s", e)
continue
if mod and hasattr(mod, "register"):
await mod.register(app)
definition = ClassLoader.load_module("definition", plugin.__name__)
if definition:
# Load plugin routes that are in a versioned package.
for plugin_version in definition.versions:
try:
mod = ClassLoader.load_module(
f"{plugin.__name__}.{plugin_version['path']}.routes"
)
except ModuleLoadError as e:
LOGGER.error("Error loading admin routes: %s", e)
continue
if mod and hasattr(mod, "register"):
await mod.register(app)
else:
# Load plugin routes that aren't in a versioned package.
try:
mod = ClassLoader.load_module(f"{plugin.__name__}.routes")
except ModuleLoadError as e:
LOGGER.error("Error loading admin routes: %s", e)
continue
if mod and hasattr(mod, "register"):
await mod.register(app)

def __repr__(self) -> str:
"""Return a string representation for this class."""
Expand Down
Loading