-
-
Notifications
You must be signed in to change notification settings - Fork 3.8k
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 experimental event ruler #10615
Add experimental event ruler #10615
Changes from 28 commits
0d89025
776c2c7
a4ea75e
6ecd057
138f6bb
055ae46
5fc3349
d2e748c
150af42
c373a7a
dda765c
38ab093
35cfaea
9e96cd2
db663b6
bcc2f13
87a6440
f330041
c2cbf23
4869491
0a77e95
531839e
b3b6b5b
d984c72
e267837
9d142d0
9a54628
1ccc192
1c37ed8
4e7dbc5
29f660d
cd22f80
8f108eb
dc265aa
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4,13 +4,13 @@ | |
from abc import ABC | ||
from functools import lru_cache | ||
from sys import version_info | ||
from typing import Optional | ||
from typing import Optional, Tuple | ||
|
||
import requests | ||
|
||
from localstack import config | ||
|
||
from ..constants import LOCALSTACK_VENV_FOLDER | ||
from ..constants import LOCALSTACK_VENV_FOLDER, MAVEN_REPO_URL | ||
from ..utils.archives import download_and_extract | ||
from ..utils.files import chmod_r, chown_r, mkdir, rm_rf | ||
from ..utils.http import download | ||
|
@@ -295,3 +295,77 @@ def _install(self, target: InstallTarget) -> None: | |
def _setup_existing_installation(self, target: InstallTarget) -> None: | ||
"""If the venv is already present, it just needs to be initialized once.""" | ||
self._prepare_installation(target) | ||
|
||
|
||
class MavenDownloadInstaller(DownloadInstaller): | ||
"""The packageURL is easy copy/pastable from the Maven central repository and the first package URL | ||
defines the package name and version. | ||
Example package_url: pkg:maven/software.amazon.event.ruler/[email protected] | ||
=> name: event-ruler | ||
=> version: 1.7.3 | ||
""" | ||
|
||
# Example: software.amazon.event.ruler | ||
group_id: str | ||
# Example: event-ruler | ||
artifact_id: str | ||
|
||
# Custom installation directory | ||
install_dir_suffix: str | None | ||
|
||
def __init__(self, package_url: str, install_dir_suffix: str | None = None): | ||
self.group_id, self.artifact_id, version = parse_maven_package_url(package_url) | ||
super().__init__(self.artifact_id, version) | ||
self.install_dir_suffix = install_dir_suffix | ||
|
||
def _get_download_url(self) -> str: | ||
group_id_path = self.group_id.replace(".", "/") | ||
return f"{MAVEN_REPO_URL}/{group_id_path}/{self.artifact_id}/{self.version}/{self.artifact_id}-{self.version}.jar" | ||
|
||
def _get_install_dir(self, target: InstallTarget) -> str: | ||
"""Allow to overwrite the default installation directory. | ||
This enables downloading transitive dependencies into the same directory. | ||
""" | ||
if self.install_dir_suffix: | ||
return os.path.join(target.value, self.install_dir_suffix) | ||
else: | ||
return super()._get_install_dir(target) | ||
|
||
|
||
class MavenPackageInstaller(MavenDownloadInstaller): | ||
"""Package installer for downloading Maven JARs, including optional dependencies. | ||
The first Maven package is used as main LPM package and other dependencies are installed additionally. | ||
Follows the Maven naming conventions: https://maven.apache.org/guides/mini/guide-naming-conventions.html | ||
""" | ||
|
||
# Installers for Maven dependencies | ||
dependencies: list[MavenDownloadInstaller] | ||
|
||
def __init__(self, *package_urls: str): | ||
super().__init__(package_urls[0]) | ||
self.dependencies = [] | ||
|
||
# Create installers for dependencies | ||
for package_url in package_urls[1:]: | ||
install_dir_suffix = os.path.join(self.name, self.version) | ||
self.dependencies.append(MavenDownloadInstaller(package_url, install_dir_suffix)) | ||
|
||
def _install(self, target: InstallTarget) -> None: | ||
# Install all dependencies first | ||
for dependency in self.dependencies: | ||
dependency._install(target) | ||
# Install the main Maven package once all dependencies are installed. | ||
# This main package indicates whether all dependencies are installed. | ||
super()._install(target) | ||
|
||
|
||
def parse_maven_package_url(package_url: str) -> Tuple[str, str, str]: | ||
"""Example: parse_maven_package_url("pkg:maven/software.amazon.event.ruler/[email protected]") | ||
-> software.amazon.event.ruler, event-ruler, 1.7.3 | ||
""" | ||
parts = package_url.split("/") | ||
group_id = parts[1] | ||
sub_parts = parts[2].split("@") | ||
artifact_id = sub_parts[0] | ||
version = sub_parts[1] | ||
return group_id, artifact_id, version |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,63 @@ | ||
import logging | ||
import os | ||
from functools import cache | ||
from pathlib import Path | ||
|
||
from localstack import config | ||
from localstack.services.events.packages import event_ruler_package | ||
from localstack.services.events.utils import InvalidEventPatternException | ||
from localstack.utils.objects import singleton_factory | ||
|
||
THIS_FOLDER = os.path.dirname(os.path.realpath(__file__)) | ||
|
||
LOG = logging.getLogger(__name__) | ||
|
||
|
||
@singleton_factory | ||
def start_jvm() -> None: | ||
import jpype | ||
from jpype import config as jpype_config | ||
|
||
# Workaround to unblock LocalStack shutdown. By default, JPype waits until all daemon threads are terminated, | ||
# which blocks the LocalStack shutdown during testing because pytest runs LocalStack in a separate thread and | ||
# `jpype.shutdownJVM()` only works from the main Python thread. | ||
# Shutting down the JVM: https://jpype.readthedocs.io/en/latest/userguide.html#shutting-down-the-jvm | ||
# JPype shutdown discussion: https://github.com/MPh-py/MPh/issues/15#issuecomment-778486669 | ||
jpype_config.destroy_jvm = False | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That's unfortunately needed. Could this cause any side-effects? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The container shutdown will probably not work, as we have an orphan process which did not get terminated by its parent on shutdown. The supervisor will probably wait forever, until docker kills the container. |
||
|
||
if not jpype.isJVMStarted(): | ||
event_ruler_libs_path = get_event_ruler_libs_path() | ||
event_ruler_libs_pattern = event_ruler_libs_path.joinpath("*") | ||
jpype.startJVM(classpath=[event_ruler_libs_pattern]) | ||
|
||
|
||
@cache | ||
def get_event_ruler_libs_path() -> Path: | ||
installer = event_ruler_package.get_installer() | ||
installer.install() | ||
return Path(installer.get_installed_dir()) | ||
|
||
|
||
def matches_rule(event: str, rule: str) -> bool: | ||
"""Invokes the AWS Event Ruler Java library: https://github.com/aws/event-ruler | ||
There is a single static boolean method Ruler.matchesRule(event, rule) - | ||
both arguments are provided as JSON strings. | ||
""" | ||
if config.EVENT_RULE_ENGINE != "java": | ||
raise NotImplementedError("Set EVENT_RULE_ENGINE=java to enable the Java Event Ruler.") | ||
|
||
start_jvm() | ||
import jpype.imports # noqa F401: required for importing Java modules | ||
from jpype import java | ||
|
||
# Import of the Java class "Ruler" needs to happen after the JVM start | ||
from software.amazon.event.ruler import Ruler | ||
|
||
try: | ||
# "Static rule matching" is the easiest implementation to get started. | ||
# "Matching with a machine" using a compiled machine is faster and enables rule validation before matching. | ||
# https://github.com/aws/event-ruler?tab=readme-ov-file#matching-with-a-machine | ||
return Ruler.matchesRule(event, rule) | ||
except java.lang.Exception as e: | ||
reason = e.args[0] | ||
raise InvalidEventPatternException(reason=reason) from e |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
from localstack.packages import Package, PackageInstaller | ||
from localstack.packages.core import MavenPackageInstaller | ||
|
||
# https://central.sonatype.com/artifact/software.amazon.event.ruler/event-ruler | ||
EVENT_RULER_VERSION = "1.7.3" | ||
# The dependent jackson.version is defined in the Maven POM File of event-ruler | ||
JACKSON_VERSION = "2.16.2" | ||
|
||
|
||
class EventRulerPackage(Package): | ||
def __init__(self): | ||
super().__init__("EventRulerLibs", EVENT_RULER_VERSION) | ||
|
||
def get_versions(self) -> list[str]: | ||
return [EVENT_RULER_VERSION] | ||
|
||
def _get_installer(self, version: str) -> PackageInstaller: | ||
return MavenPackageInstaller( | ||
f"pkg:maven/software.amazon.event.ruler/event-ruler@{EVENT_RULER_VERSION}", | ||
f"pkg:maven/com.fasterxml.jackson.core/jackson-annotations@{JACKSON_VERSION}", | ||
f"pkg:maven/com.fasterxml.jackson.core/jackson-core@{JACKSON_VERSION}", | ||
f"pkg:maven/com.fasterxml.jackson.core/jackson-databind@{JACKSON_VERSION}", | ||
) | ||
|
||
|
||
event_ruler_package = EventRulerPackage() |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -67,6 +67,8 @@ base-runtime = [ | |
"Werkzeug>=3.0.0", | ||
"xmltodict>=0.13.0", | ||
"rolo>=0.4", | ||
# allow Python programs full access to Java class libraries. Used for opt-in event ruler. | ||
"JPype1>=1.5.0", | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 431KB without any extra dependencies (see their pyproject.toml), just There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. could we move it to For a bit of context, basically, This would also have the side-effect of not having to change the S3 image Dockerfile, which does not need this and g++. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That makes a lot of sense @bentsku 💯 Thank you for this valuable input 👀 👏 I didn't understand the full implications of With the fixed solution, the S3 image does not get larger 👌 |
||
# TODO: remove those 2 dependencies | ||
"flask>=3.0.0", | ||
"Quart>=0.19.2", | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
✅ CI run with JPype enabled using
java
here in 017bae5