Skip to content

Commit

Permalink
Merge pull request #443 from nrempel/proto-versions
Browse files Browse the repository at this point in the history
Add support for major/minor protocol version routing
  • Loading branch information
andrewwhitehead authored Apr 17, 2020
2 parents 0961603 + 4a10237 commit cc7cb35
Show file tree
Hide file tree
Showing 199 changed files with 1,276 additions and 371 deletions.
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

0 comments on commit cc7cb35

Please sign in to comment.