insightconnect-plugin-runtime 6.2.6__py3-none-any.whl → 6.3.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- insightconnect_plugin_runtime/api/endpoints.py +6 -6
- insightconnect_plugin_runtime/server.py +29 -19
- insightconnect_plugin_runtime/telemetry.py +73 -0
- insightconnect_plugin_runtime/util.py +1 -0
- {insightconnect_plugin_runtime-6.2.6.dist-info → insightconnect_plugin_runtime-6.3.0.dist-info}/METADATA +8 -2
- {insightconnect_plugin_runtime-6.2.6.dist-info → insightconnect_plugin_runtime-6.3.0.dist-info}/RECORD +9 -8
- {insightconnect_plugin_runtime-6.2.6.dist-info → insightconnect_plugin_runtime-6.3.0.dist-info}/WHEEL +1 -1
- tests/unit/test_server_cloud_plugins.py +9 -4
- {insightconnect_plugin_runtime-6.2.6.dist-info → insightconnect_plugin_runtime-6.3.0.dist-info}/top_level.txt +0 -0
|
@@ -787,7 +787,7 @@ class Endpoints:
|
|
|
787
787
|
return version
|
|
788
788
|
|
|
789
789
|
def add_plugin_custom_config(
|
|
790
|
-
|
|
790
|
+
self, input_data: Dict[str, Any], org_id: str
|
|
791
791
|
) -> Dict[str, Any]:
|
|
792
792
|
"""
|
|
793
793
|
Using the retrieved configs pulled from komand-props, pass the configuration that matches the requesting
|
|
@@ -815,7 +815,7 @@ class Endpoints:
|
|
|
815
815
|
# This means we still need to manually delete the state for plugins on a per org basis.
|
|
816
816
|
# This also means first time customers for their 'initial' lookup would get the lookback value passed in.
|
|
817
817
|
if input_data.get("body", {}).get("state") and additional_config.get(
|
|
818
|
-
|
|
818
|
+
"lookback"
|
|
819
819
|
):
|
|
820
820
|
self.logger.info(
|
|
821
821
|
"Found an existing plugin state, not passing lookback value..."
|
|
@@ -842,8 +842,8 @@ class Endpoints:
|
|
|
842
842
|
if isinstance(wrapped_exception, ClientException):
|
|
843
843
|
status_code = 400
|
|
844
844
|
elif (
|
|
845
|
-
|
|
846
|
-
|
|
845
|
+
isinstance(wrapped_exception, PluginException)
|
|
846
|
+
and wrapped_exception.preset is PluginException.Preset.BAD_REQUEST
|
|
847
847
|
):
|
|
848
848
|
status_code = 400
|
|
849
849
|
elif isinstance(wrapped_exception, (ConnectionTestException, ClientException)):
|
|
@@ -868,8 +868,8 @@ class Endpoints:
|
|
|
868
868
|
if isinstance(wrapped_exception, (ConnectionTestException, ClientException)):
|
|
869
869
|
return 400
|
|
870
870
|
elif (
|
|
871
|
-
|
|
872
|
-
|
|
871
|
+
isinstance(wrapped_exception, PluginException)
|
|
872
|
+
and wrapped_exception.preset is PluginException.Preset.BAD_REQUEST
|
|
873
873
|
):
|
|
874
874
|
return 400
|
|
875
875
|
elif isinstance(wrapped_exception, ServerException):
|
|
@@ -1,17 +1,16 @@
|
|
|
1
|
-
import os
|
|
2
|
-
import sys
|
|
3
1
|
import json
|
|
4
2
|
import logging
|
|
3
|
+
import os
|
|
4
|
+
import sys
|
|
5
|
+
from time import sleep
|
|
5
6
|
|
|
7
|
+
import gunicorn.app.base
|
|
8
|
+
import structlog
|
|
6
9
|
from apispec import APISpec
|
|
7
10
|
from apispec.ext.marshmallow import MarshmallowPlugin
|
|
8
11
|
from apispec_webframeworks.flask import FlaskPlugin
|
|
9
|
-
|
|
10
12
|
from flask import Flask, request_started, request
|
|
11
|
-
import gunicorn.app.base
|
|
12
13
|
from gunicorn.arbiter import Arbiter
|
|
13
|
-
|
|
14
|
-
import structlog
|
|
15
14
|
from pythonjsonlogger.jsonlogger import JsonFormatter
|
|
16
15
|
from requests import get as request_get
|
|
17
16
|
from requests.exceptions import (
|
|
@@ -21,9 +20,9 @@ from requests.exceptions import (
|
|
|
21
20
|
JSONDecodeError,
|
|
22
21
|
ConnectionError,
|
|
23
22
|
)
|
|
24
|
-
from time import sleep
|
|
25
23
|
from werkzeug.utils import secure_filename
|
|
26
24
|
|
|
25
|
+
from insightconnect_plugin_runtime.api.endpoints import Endpoints, handle_errors
|
|
27
26
|
from insightconnect_plugin_runtime.api.schemas import (
|
|
28
27
|
PluginInfoSchema,
|
|
29
28
|
ActionTriggerOutputBodySchema,
|
|
@@ -39,9 +38,9 @@ from insightconnect_plugin_runtime.api.schemas import (
|
|
|
39
38
|
ConnectionDetailsSchema,
|
|
40
39
|
ConnectionTestSchema,
|
|
41
40
|
)
|
|
42
|
-
from insightconnect_plugin_runtime.api.endpoints import Endpoints, handle_errors
|
|
43
|
-
from insightconnect_plugin_runtime.util import is_running_in_cloud
|
|
44
41
|
from insightconnect_plugin_runtime.helper import clean_dict
|
|
42
|
+
from insightconnect_plugin_runtime.telemetry import create_post_fork
|
|
43
|
+
from insightconnect_plugin_runtime.util import is_running_in_cloud, OTEL_ENDPOINT
|
|
45
44
|
|
|
46
45
|
API_TITLE = "InsightConnect Plugin Runtime API"
|
|
47
46
|
API_VERSION = "1.0"
|
|
@@ -72,14 +71,14 @@ class PluginServer(gunicorn.app.base.BaseApplication):
|
|
|
72
71
|
"""
|
|
73
72
|
|
|
74
73
|
def __init__(
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
74
|
+
self,
|
|
75
|
+
plugin,
|
|
76
|
+
port=10001,
|
|
77
|
+
workers=1,
|
|
78
|
+
threads=4,
|
|
79
|
+
debug=False,
|
|
80
|
+
worker_class="sync",
|
|
81
|
+
worker_connections=200,
|
|
83
82
|
):
|
|
84
83
|
|
|
85
84
|
gunicorn_file = os.environ.get("GUNICORN_CONFIG_FILE")
|
|
@@ -120,6 +119,9 @@ class PluginServer(gunicorn.app.base.BaseApplication):
|
|
|
120
119
|
self.get_plugin_properties_from_cps()
|
|
121
120
|
self.app, self.blueprints = self.create_flask_app()
|
|
122
121
|
|
|
122
|
+
if os.environ.get(OTEL_ENDPOINT):
|
|
123
|
+
self.config_options[OTEL_ENDPOINT] = os.environ[OTEL_ENDPOINT]
|
|
124
|
+
|
|
123
125
|
@staticmethod
|
|
124
126
|
def configure_structlog_instance(is_debug: bool) -> None:
|
|
125
127
|
structlog.configure(
|
|
@@ -166,6 +168,9 @@ class PluginServer(gunicorn.app.base.BaseApplication):
|
|
|
166
168
|
for key, value in config.items():
|
|
167
169
|
self.cfg.set(key.lower(), value)
|
|
168
170
|
|
|
171
|
+
post_fork = create_post_fork(lambda: self.app, lambda: self.plugin, lambda: self.config_options)
|
|
172
|
+
self.cfg.set("post_fork", post_fork)
|
|
173
|
+
|
|
169
174
|
def create_flask_app(self):
|
|
170
175
|
app = Flask(__name__)
|
|
171
176
|
|
|
@@ -193,7 +198,7 @@ class PluginServer(gunicorn.app.base.BaseApplication):
|
|
|
193
198
|
|
|
194
199
|
def get_plugin_properties_from_cps(self):
|
|
195
200
|
# Call out to komand-props to get configurations related to only the plugin pod running.
|
|
196
|
-
if is_running_in_cloud()
|
|
201
|
+
if is_running_in_cloud():
|
|
197
202
|
for attempt in range(1, CPS_RETRY + 1):
|
|
198
203
|
self.logger.info(
|
|
199
204
|
f"Getting plugin configuration information... (attempt {attempt}/{CPS_RETRY})"
|
|
@@ -206,7 +211,12 @@ class PluginServer(gunicorn.app.base.BaseApplication):
|
|
|
206
211
|
) # match how we name our images
|
|
207
212
|
plugin_config = resp_json.get("plugins", {}).get(plugin, {})
|
|
208
213
|
|
|
209
|
-
self.
|
|
214
|
+
if self.plugin.tasks:
|
|
215
|
+
self.config_options = plugin_config
|
|
216
|
+
|
|
217
|
+
if resp_json.get(OTEL_ENDPOINT, {}):
|
|
218
|
+
self.config_options[OTEL_ENDPOINT] = resp_json.get(OTEL_ENDPOINT, {})
|
|
219
|
+
|
|
210
220
|
self.logger.info("Plugin configuration successfully retrieved...")
|
|
211
221
|
return
|
|
212
222
|
except MissingSchema as missing_schema:
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import functools
|
|
2
|
+
from typing import Any, Callable
|
|
3
|
+
from flask.app import Flask
|
|
4
|
+
|
|
5
|
+
from opentelemetry import trace
|
|
6
|
+
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
|
|
7
|
+
from opentelemetry.instrumentation.flask import FlaskInstrumentor
|
|
8
|
+
from opentelemetry.instrumentation.requests import RequestsInstrumentor
|
|
9
|
+
from opentelemetry.sdk.resources import Resource
|
|
10
|
+
from opentelemetry.sdk.trace import TracerProvider
|
|
11
|
+
from opentelemetry.sdk.trace.export import BatchSpanProcessor
|
|
12
|
+
from opentelemetry.trace import Status, StatusCode
|
|
13
|
+
|
|
14
|
+
from insightconnect_plugin_runtime.plugin import Plugin
|
|
15
|
+
from insightconnect_plugin_runtime.util import is_running_in_cloud, OTEL_ENDPOINT
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def init_tracing(app: Flask, plugin: Plugin, endpoint: str) -> None:
|
|
19
|
+
"""
|
|
20
|
+
Initialize OpenTelemetry Tracing
|
|
21
|
+
|
|
22
|
+
The function sets up the tracer provider, span processor and exporter with auto-instrumentation
|
|
23
|
+
|
|
24
|
+
:param app: The Flask Application
|
|
25
|
+
:param plugin: The plugin to derive the service name from
|
|
26
|
+
:param endpoint: The Otel Endpoint to emit traces to
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
if not is_running_in_cloud():
|
|
30
|
+
return
|
|
31
|
+
|
|
32
|
+
resource = Resource(attributes={"service.name": f'{plugin.name.lower().replace(" ", "_")}-{plugin.version}'})
|
|
33
|
+
|
|
34
|
+
trace_provider = TracerProvider(resource=resource)
|
|
35
|
+
exporter = OTLPSpanExporter(endpoint=endpoint)
|
|
36
|
+
trace_provider.add_span_processor(BatchSpanProcessor(exporter))
|
|
37
|
+
trace.set_tracer_provider(trace_provider)
|
|
38
|
+
|
|
39
|
+
FlaskInstrumentor().instrument_app(app)
|
|
40
|
+
|
|
41
|
+
def requests_callback(span: trace.Span, _: Any, response: Any) -> None:
|
|
42
|
+
if hasattr(response, "status_code"):
|
|
43
|
+
span.set_status(Status(StatusCode.OK if response.status_code < 400 else StatusCode.ERROR))
|
|
44
|
+
|
|
45
|
+
RequestsInstrumentor().instrument(trace_provider=trace_provider, response_hook=requests_callback)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def auto_instrument(func: Callable) -> Callable:
|
|
49
|
+
"""
|
|
50
|
+
Decorator that auto-instruments a function with a trace
|
|
51
|
+
|
|
52
|
+
:param func: function to instrument
|
|
53
|
+
:return:
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
@functools.wraps(func)
|
|
57
|
+
def wrapper(*args, **kwargs):
|
|
58
|
+
tracer = trace.get_tracer(__name__)
|
|
59
|
+
with tracer.start_as_current_span(func.__name__):
|
|
60
|
+
return func(*args, **kwargs)
|
|
61
|
+
|
|
62
|
+
return wrapper
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def create_post_fork(app_getter: Callable, plugin_getter: Callable, config_getter: Callable) -> Callable:
|
|
66
|
+
def post_fork(server, worker):
|
|
67
|
+
app = app_getter()
|
|
68
|
+
plugin = plugin_getter()
|
|
69
|
+
endpoint = config_getter().get(OTEL_ENDPOINT, None)
|
|
70
|
+
if endpoint:
|
|
71
|
+
init_tracing(app, plugin, endpoint)
|
|
72
|
+
|
|
73
|
+
return post_fork
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: insightconnect-plugin-runtime
|
|
3
|
-
Version: 6.
|
|
3
|
+
Version: 6.3.0
|
|
4
4
|
Summary: InsightConnect Plugin Runtime
|
|
5
5
|
Home-page: https://github.com/rapid7/komand-plugin-sdk-python
|
|
6
6
|
Author: Rapid7 Integrations Alliance
|
|
@@ -26,6 +26,11 @@ Requires-Dist: apispec-webframeworks==1.0.0
|
|
|
26
26
|
Requires-Dist: blinker==1.9.0
|
|
27
27
|
Requires-Dist: structlog==24.4.0
|
|
28
28
|
Requires-Dist: python-json-logger==2.0.7
|
|
29
|
+
Requires-Dist: Jinja2==3.1.6
|
|
30
|
+
Requires-Dist: opentelemetry-sdk==1.31.1
|
|
31
|
+
Requires-Dist: opentelemetry-instrumentation-flask==0.52b1
|
|
32
|
+
Requires-Dist: opentelemetry-exporter-otlp-proto-http==1.31.1
|
|
33
|
+
Requires-Dist: opentelemetry-instrumentation-requests==0.52b1
|
|
29
34
|
Dynamic: author
|
|
30
35
|
Dynamic: author-email
|
|
31
36
|
Dynamic: classifier
|
|
@@ -219,6 +224,7 @@ contributed. Black is installed as a test dependency and the hook can be initial
|
|
|
219
224
|
after cloning this repository.
|
|
220
225
|
|
|
221
226
|
## Changelog
|
|
227
|
+
* 6.3.0 - Add Tracing Instrumentation
|
|
222
228
|
* 6.2.6 - Remove setuptools after installation
|
|
223
229
|
* 6.2.5 - Fixed bug related to failure to set default region to assume role method in `aws_client` for newer versions of boto3 | Updated alpine image packages on build
|
|
224
230
|
* 6.2.4 - Update `make_request` helper to support extra parameter of `max_response_size` to cap the response
|
|
@@ -8,14 +8,15 @@ insightconnect_plugin_runtime/helper.py,sha256=B0XqAXmn8CT1KQ6i5IoWLQrQ_HVOvuKrI
|
|
|
8
8
|
insightconnect_plugin_runtime/metrics.py,sha256=hf_Aoufip_s4k4o8Gtzz90ymZthkaT2e5sXh5B4LcF0,3186
|
|
9
9
|
insightconnect_plugin_runtime/plugin.py,sha256=Yf4LNczykDVc31F9G8uuJ9gxEsgmxmAr0n4pcZzichM,26393
|
|
10
10
|
insightconnect_plugin_runtime/schema.py,sha256=6MVw5hqGATU1VLgwfOWfPsP3hy1OnsugCTsgX8sknes,521
|
|
11
|
-
insightconnect_plugin_runtime/server.py,sha256=
|
|
11
|
+
insightconnect_plugin_runtime/server.py,sha256=DHooHBQa1M2z7aETTWK6u9B2jChi8vONzqK4n_c94f4,13138
|
|
12
12
|
insightconnect_plugin_runtime/step.py,sha256=KdERg-789-s99IEKN61DR08naz-YPxyinPT0C_T81C4,855
|
|
13
13
|
insightconnect_plugin_runtime/task.py,sha256=d-H1EAzVnmSdDEJtXyIK5JySprxpF9cetVoFGtWlHrg,123
|
|
14
|
+
insightconnect_plugin_runtime/telemetry.py,sha256=6JgZOyhB0qcDYKPlQZ47M2ZPfDpfAE_zuAhBtw4GMvo,2586
|
|
14
15
|
insightconnect_plugin_runtime/trigger.py,sha256=Zq3cy68N3QxAGbNZKCID6CZF05Zi7YD2sdy_qbedUY8,874
|
|
15
|
-
insightconnect_plugin_runtime/util.py,sha256=
|
|
16
|
+
insightconnect_plugin_runtime/util.py,sha256=8cle29INhnshEcL2LWpaC0ZGqevjq8pW8TE0MFEiYYw,8475
|
|
16
17
|
insightconnect_plugin_runtime/variables.py,sha256=7FjJGnU7KUR7m9o-_tRq7Q3KiaB1Pp0Apj1NGgOwrJk,3056
|
|
17
18
|
insightconnect_plugin_runtime/api/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
18
|
-
insightconnect_plugin_runtime/api/endpoints.py,sha256=
|
|
19
|
+
insightconnect_plugin_runtime/api/endpoints.py,sha256=Kn9VfnpvcVO9oY5W07laJkXFNp0PfZQrY2AecYFbvNw,33377
|
|
19
20
|
insightconnect_plugin_runtime/api/schemas.py,sha256=jRmDrwLJTBl-iQOnyZkSwyJlCWg4eNjAnKfD9Eko4z0,2754
|
|
20
21
|
insightconnect_plugin_runtime/clients/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
21
22
|
insightconnect_plugin_runtime/clients/aws_client.py,sha256=ViJF3klbD1YM5GVxme3BFYfpuADUlVGP8sdbX2Z6_Cw,23061
|
|
@@ -73,12 +74,12 @@ tests/unit/test_metrics.py,sha256=PjjTrB9w7uQ2Q5UN-893-SsH3EGJuBseOMHSD1I004s,79
|
|
|
73
74
|
tests/unit/test_oauth.py,sha256=nbFG0JH1x04ExXqSe-b5BGdt_hJs7DP17eUa6bQzcYI,2093
|
|
74
75
|
tests/unit/test_plugin.py,sha256=ZTNAZWwZhDIAbxkVuWhnz9FzmojbijgMmsLWM2mXQI0,4160
|
|
75
76
|
tests/unit/test_schema.py,sha256=swWZPRo_Q4M6VHte-srmxcV2wH-XS7pgmNRxpaL0Qrg,642
|
|
76
|
-
tests/unit/test_server_cloud_plugins.py,sha256=
|
|
77
|
+
tests/unit/test_server_cloud_plugins.py,sha256=3hEdNrJmnDXqhxhd4uSdTJ0ksn2S11RsNdZCmnethCw,5340
|
|
77
78
|
tests/unit/test_server_spec.py,sha256=je97BaktgK0Fiz3AwFPkcmHzYtOJJNqJV_Fw5hrvqX4,644
|
|
78
79
|
tests/unit/test_trigger.py,sha256=E53mAUoVyponWu_4IQZ0IC1gQ9lakBnTn_9vKN2IZfg,1692
|
|
79
80
|
tests/unit/test_variables.py,sha256=OUEOqGYZA3Nd5oKk5GVY3hcrWKHpZpxysBJcO_v5gzs,291
|
|
80
81
|
tests/unit/utils.py,sha256=hcY0A2H_DMgCDXUTvDtCXMdMvRjLQgTaGcTpATb8YG0,2236
|
|
81
|
-
insightconnect_plugin_runtime-6.
|
|
82
|
-
insightconnect_plugin_runtime-6.
|
|
83
|
-
insightconnect_plugin_runtime-6.
|
|
84
|
-
insightconnect_plugin_runtime-6.
|
|
82
|
+
insightconnect_plugin_runtime-6.3.0.dist-info/METADATA,sha256=GCmcMcuLriTNb5PMLZc83UqlpxByddurw6G-WAwui-4,16225
|
|
83
|
+
insightconnect_plugin_runtime-6.3.0.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
|
|
84
|
+
insightconnect_plugin_runtime-6.3.0.dist-info/top_level.txt,sha256=AJtyJOpiFzHxsbHUICTcUKXyrGQ3tZxhrEHsPjJBvEA,36
|
|
85
|
+
insightconnect_plugin_runtime-6.3.0.dist-info/RECORD,,
|
|
@@ -4,6 +4,7 @@ from unittest import TestCase, skip
|
|
|
4
4
|
from unittest.mock import patch, MagicMock
|
|
5
5
|
|
|
6
6
|
from insightconnect_plugin_runtime.server import PluginServer
|
|
7
|
+
from insightconnect_plugin_runtime.util import OTEL_ENDPOINT
|
|
7
8
|
from tests.plugin.hello_world import KomandHelloWorld
|
|
8
9
|
from .utils import MockResponse, Logger
|
|
9
10
|
|
|
@@ -20,15 +21,19 @@ class TestServerCloudPlugins(TestCase):
|
|
|
20
21
|
@parameterized.expand([["Set cloud to false", False], ["Set cloud to true", True]])
|
|
21
22
|
@patch("insightconnect_plugin_runtime.server.request_get")
|
|
22
23
|
def test_cloud_plugin_no_tasks_ignore_cps(self, _test_name, cloud, mocked_req, mock_cloud, _run):
|
|
24
|
+
fake_endpoint = "http://fake.endpoint.com"
|
|
25
|
+
mocked_req.return_value = MockResponse({OTEL_ENDPOINT: fake_endpoint}) if cloud else MockResponse({})
|
|
23
26
|
mock_cloud.return_value = cloud # Mock plugin running in cloud vs not
|
|
24
27
|
self.plugin.tasks = None # ensure still no tasks as other tests edit this and could fail before reverting
|
|
25
|
-
plugin_server = PluginServer(self.plugin) # this plugin has no tasks by default
|
|
26
28
|
|
|
29
|
+
plugin_server = PluginServer(self.plugin) # this plugin has no tasks by default
|
|
27
30
|
plugin_server.start()
|
|
28
|
-
self.assertEqual(plugin_server.config_options, {})
|
|
29
31
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
+
self.assertEqual(plugin_server.config_options, {OTEL_ENDPOINT: fake_endpoint} if cloud else {})
|
|
33
|
+
|
|
34
|
+
# Plugin server calls out to CPS when cloud to get tracing endpoint
|
|
35
|
+
self.assertEqual(mocked_req.called, cloud)
|
|
36
|
+
|
|
32
37
|
|
|
33
38
|
@patch("insightconnect_plugin_runtime.server.request_get")
|
|
34
39
|
def test_cloud_plugin_calls_cps(self, mocked_req, _mock_cloud, _run):
|
|
File without changes
|