insightconnect-plugin-runtime 5.4.0__py3-none-any.whl → 5.4.2__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/server.py +26 -39
- {insightconnect_plugin_runtime-5.4.0.dist-info → insightconnect_plugin_runtime-5.4.2.dist-info}/METADATA +4 -3
- {insightconnect_plugin_runtime-5.4.0.dist-info → insightconnect_plugin_runtime-5.4.2.dist-info}/RECORD +6 -6
- tests/unit/test_server_cloud_plugins.py +10 -22
- {insightconnect_plugin_runtime-5.4.0.dist-info → insightconnect_plugin_runtime-5.4.2.dist-info}/WHEEL +0 -0
- {insightconnect_plugin_runtime-5.4.0.dist-info → insightconnect_plugin_runtime-5.4.2.dist-info}/top_level.txt +0 -0
|
@@ -6,8 +6,6 @@ import logging
|
|
|
6
6
|
from apispec import APISpec
|
|
7
7
|
from apispec.ext.marshmallow import MarshmallowPlugin
|
|
8
8
|
from apispec_webframeworks.flask import FlaskPlugin
|
|
9
|
-
from apscheduler.schedulers.background import BackgroundScheduler
|
|
10
|
-
|
|
11
9
|
|
|
12
10
|
from flask import Flask, request_started, request
|
|
13
11
|
import gunicorn.app.base
|
|
@@ -17,6 +15,7 @@ import structlog
|
|
|
17
15
|
from pythonjsonlogger.jsonlogger import JsonFormatter
|
|
18
16
|
from requests import get as request_get
|
|
19
17
|
from requests.exceptions import HTTPError, MissingSchema, Timeout, JSONDecodeError
|
|
18
|
+
from time import sleep
|
|
20
19
|
from werkzeug.utils import secure_filename
|
|
21
20
|
|
|
22
21
|
from insightconnect_plugin_runtime.api.schemas import (
|
|
@@ -42,6 +41,8 @@ API_TITLE = "InsightConnect Plugin Runtime API"
|
|
|
42
41
|
API_VERSION = "1.0"
|
|
43
42
|
OPEN_API_VERSION = "2.0"
|
|
44
43
|
VERSION_MAPPING = {"legacy": "/", "v1": "/api/v1"}
|
|
44
|
+
CPS_RETRY = 5
|
|
45
|
+
RETRY_SLEEP = 10
|
|
45
46
|
CPS_ENDPOINT = os.getenv("CPS_ENDPOINT") # endpoint to retrieve plugin custom configs (set in cloud deployments.tf)
|
|
46
47
|
DEFAULT_SCHEDULE_INTERVAL_MINUTES = 3 # default to 3 as most integrations run on 5 minute interval
|
|
47
48
|
|
|
@@ -105,7 +106,7 @@ class PluginServer(gunicorn.app.base.BaseApplication):
|
|
|
105
106
|
self.workers = workers
|
|
106
107
|
self.threads = threads
|
|
107
108
|
# initialise before reaching out to CPS for configured values
|
|
108
|
-
self.config_options
|
|
109
|
+
self.config_options = {}
|
|
109
110
|
self.get_plugin_properties_from_cps()
|
|
110
111
|
self.app, self.blueprints = self.create_flask_app()
|
|
111
112
|
|
|
@@ -183,25 +184,28 @@ class PluginServer(gunicorn.app.base.BaseApplication):
|
|
|
183
184
|
def get_plugin_properties_from_cps(self):
|
|
184
185
|
# Call out to komand-props to get configurations related to only the plugin pod running.
|
|
185
186
|
if is_running_in_cloud() and self.plugin.tasks:
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
187
|
+
for attempt in range(1, CPS_RETRY + 1):
|
|
188
|
+
self.logger.info(f"Getting plugin configuration information... (attempt {attempt}/{CPS_RETRY})")
|
|
189
|
+
try:
|
|
190
|
+
request_response = request_get(CPS_ENDPOINT, timeout=30)
|
|
191
|
+
resp_json = request_response.json()
|
|
192
|
+
plugin = self.plugin.name.lower().replace(" ", "_") # match how we name our images
|
|
193
|
+
plugin_config = resp_json.get("plugins", {}).get(plugin, {})
|
|
194
|
+
|
|
195
|
+
self.config_options = plugin_config
|
|
196
|
+
self.logger.info("Plugin configuration successfully retrieved...")
|
|
197
|
+
break
|
|
198
|
+
except MissingSchema as missing_schema:
|
|
199
|
+
self.logger.error(f"Invalid URL being requested: {CPS_ENDPOINT}, error={missing_schema}")
|
|
200
|
+
except Timeout as timeout:
|
|
201
|
+
self.logger.error(f"Connection timeout hit. CPS={CPS_ENDPOINT}, error={timeout}")
|
|
202
|
+
except HTTPError as http_error:
|
|
203
|
+
self.logger.error(f"Connection error when trying to reach CPS. CPS={CPS_ENDPOINT}, error={http_error}")
|
|
204
|
+
except JSONDecodeError as bad_json:
|
|
205
|
+
self.logger.error(f"Got bad JSON back. Response content={request_response.content}, error={bad_json}")
|
|
206
|
+
except Exception as error:
|
|
207
|
+
self.logger.error(f"Hit an unexpected error when retrieving plugin custom configs, error={error}")
|
|
208
|
+
sleep(RETRY_SLEEP)
|
|
205
209
|
|
|
206
210
|
def register_api_spec(self):
|
|
207
211
|
"""Register all swagger schema definitions and path objects"""
|
|
@@ -253,21 +257,6 @@ class PluginServer(gunicorn.app.base.BaseApplication):
|
|
|
253
257
|
blueprint, url_prefix=VERSION_MAPPING[blueprint.name]
|
|
254
258
|
)
|
|
255
259
|
|
|
256
|
-
def register_scheduled_tasks(self):
|
|
257
|
-
# Once Flask server is up and running also start the get_plugin_configs
|
|
258
|
-
try:
|
|
259
|
-
if self.plugin.tasks and self.schedule_interval:
|
|
260
|
-
self.logger.info(f"Starting scheduled task(s) to run at interval of {self.schedule_interval} minutes...")
|
|
261
|
-
scheduler = BackgroundScheduler(daemon=True)
|
|
262
|
-
scheduler.add_job(self.get_plugin_properties_from_cps, 'interval', minutes=self.schedule_interval)
|
|
263
|
-
scheduler.start()
|
|
264
|
-
else:
|
|
265
|
-
reason = "No tasks found found within plugin," if self.schedule_interval else "No schedule defined,"
|
|
266
|
-
self.logger.info(f"{reason},not starting scheduled tasks...")
|
|
267
|
-
except Exception as exception:
|
|
268
|
-
self.logger.error("Unable to start up scheduler, plugin will not be refreshing these configuration values."
|
|
269
|
-
f"Error={exception}", exc_info=True)
|
|
270
|
-
|
|
271
260
|
@staticmethod
|
|
272
261
|
def bind_request_details(sender: Flask, **extras) -> None:
|
|
273
262
|
"""
|
|
@@ -299,8 +288,6 @@ class PluginServer(gunicorn.app.base.BaseApplication):
|
|
|
299
288
|
try:
|
|
300
289
|
self.register_blueprint()
|
|
301
290
|
self.register_api_spec()
|
|
302
|
-
if is_running_in_cloud():
|
|
303
|
-
self.register_scheduled_tasks()
|
|
304
291
|
self.arbiter.run()
|
|
305
292
|
except RuntimeError as e:
|
|
306
293
|
sys.stderr.write("\nError: %s\n" % e)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: insightconnect-plugin-runtime
|
|
3
|
-
Version: 5.4.
|
|
3
|
+
Version: 5.4.2
|
|
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,7 +26,6 @@ Requires-Dist: apispec-webframeworks ==1.0.0
|
|
|
26
26
|
Requires-Dist: blinker ==1.7.0
|
|
27
27
|
Requires-Dist: structlog ==24.1.0
|
|
28
28
|
Requires-Dist: python-json-logger ==2.0.7
|
|
29
|
-
Requires-Dist: APScheduler ==3.10.4
|
|
30
29
|
|
|
31
30
|
|
|
32
31
|
# InsightConnect Python Plugin Runtime 
|
|
@@ -186,7 +185,7 @@ Running a specific test file:
|
|
|
186
185
|
| | Plugin | Slim Plugin |
|
|
187
186
|
|:------------------|:-------:|:-----------:|
|
|
188
187
|
| Python Version | 3.9.18 | 3.9.18 |
|
|
189
|
-
| OS | Alpine |
|
|
188
|
+
| OS | Alpine | Bullseye |
|
|
190
189
|
| Package installer | apk | apt |
|
|
191
190
|
| Shell | /bin/sh | /bin/bash |
|
|
192
191
|
| Image Size | ~350MB | ~180MB |
|
|
@@ -213,6 +212,8 @@ after cloning this repository.
|
|
|
213
212
|
|
|
214
213
|
## Changelog
|
|
215
214
|
|
|
215
|
+
* 5.4.2 - Remove background scheduler to simplify when the server gets custom_config values | Revert to use `bullseye` for slim image.
|
|
216
|
+
* 5.4.1 - Retry logic added to get values from external API for custom_config values.
|
|
216
217
|
* 5.4.0 - Implementation of custom_config parameter for plugin tasks | Alpine image updated OpenSSL and expat | Use `bookworm` for slim image.
|
|
217
218
|
* 5.3.2 - Updated OpenSSL in alpine image and core packages to latest versions.
|
|
218
219
|
* 5.3.1 - New logging added to the beginning and end of a task | New logging when an exception is instantiated.
|
|
@@ -8,7 +8,7 @@ insightconnect_plugin_runtime/helper.py,sha256=m5PxN04-NPXM1X10S2wwjqmiLvnNntd6T
|
|
|
8
8
|
insightconnect_plugin_runtime/metrics.py,sha256=hf_Aoufip_s4k4o8Gtzz90ymZthkaT2e5sXh5B4LcF0,3186
|
|
9
9
|
insightconnect_plugin_runtime/plugin.py,sha256=A6GMrYTNFpyCeEP00jRbp89Xu6KdQFq0HOVWV2AxA6Q,22969
|
|
10
10
|
insightconnect_plugin_runtime/schema.py,sha256=jTNc6KAMqFpaDVWrAYhkVC6e8I63P3X7uVlJkAr1hiY,583
|
|
11
|
-
insightconnect_plugin_runtime/server.py,sha256=
|
|
11
|
+
insightconnect_plugin_runtime/server.py,sha256=vm0fJcnaI9Z3WYB37wUFkOjcym_kMlSm3lAyFP3ARiM,11771
|
|
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
14
|
insightconnect_plugin_runtime/trigger.py,sha256=Zq3cy68N3QxAGbNZKCID6CZF05Zi7YD2sdy_qbedUY8,874
|
|
@@ -73,12 +73,12 @@ tests/unit/test_metrics.py,sha256=PjjTrB9w7uQ2Q5UN-893-SsH3EGJuBseOMHSD1I004s,79
|
|
|
73
73
|
tests/unit/test_oauth.py,sha256=nbFG0JH1x04ExXqSe-b5BGdt_hJs7DP17eUa6bQzcYI,2093
|
|
74
74
|
tests/unit/test_plugin.py,sha256=ZTNAZWwZhDIAbxkVuWhnz9FzmojbijgMmsLWM2mXQI0,4160
|
|
75
75
|
tests/unit/test_schema.py,sha256=swWZPRo_Q4M6VHte-srmxcV2wH-XS7pgmNRxpaL0Qrg,642
|
|
76
|
-
tests/unit/test_server_cloud_plugins.py,sha256=
|
|
76
|
+
tests/unit/test_server_cloud_plugins.py,sha256=GvJCOgpaxw3jOz_ABCkui_Ymai0kAZK1MqR0ldERnhw,4544
|
|
77
77
|
tests/unit/test_server_spec.py,sha256=je97BaktgK0Fiz3AwFPkcmHzYtOJJNqJV_Fw5hrvqX4,644
|
|
78
78
|
tests/unit/test_trigger.py,sha256=E53mAUoVyponWu_4IQZ0IC1gQ9lakBnTn_9vKN2IZfg,1692
|
|
79
79
|
tests/unit/test_variables.py,sha256=OUEOqGYZA3Nd5oKk5GVY3hcrWKHpZpxysBJcO_v5gzs,291
|
|
80
80
|
tests/unit/utils.py,sha256=-GB3nz4YJ2nMm0AR7Fm4lCNJMHCWOmLtNI1N4b9hRdo,470
|
|
81
|
-
insightconnect_plugin_runtime-5.4.
|
|
82
|
-
insightconnect_plugin_runtime-5.4.
|
|
83
|
-
insightconnect_plugin_runtime-5.4.
|
|
84
|
-
insightconnect_plugin_runtime-5.4.
|
|
81
|
+
insightconnect_plugin_runtime-5.4.2.dist-info/METADATA,sha256=8h98fhwRRCE9c7y4U6-96Qw7ci0h89KqvnrS2r-XPg0,12472
|
|
82
|
+
insightconnect_plugin_runtime-5.4.2.dist-info/WHEEL,sha256=oiQVh_5PnQM0E3gPdiz09WCNmwiHDMaGer_elqB3coM,92
|
|
83
|
+
insightconnect_plugin_runtime-5.4.2.dist-info/top_level.txt,sha256=AJtyJOpiFzHxsbHUICTcUKXyrGQ3tZxhrEHsPjJBvEA,36
|
|
84
|
+
insightconnect_plugin_runtime-5.4.2.dist-info/RECORD,,
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
from requests.exceptions import HTTPError, Timeout, TooManyRedirects
|
|
2
2
|
from parameterized import parameterized
|
|
3
|
-
from unittest import TestCase
|
|
3
|
+
from unittest import TestCase, skip
|
|
4
4
|
from unittest.mock import patch, MagicMock
|
|
5
5
|
|
|
6
|
-
from insightconnect_plugin_runtime.server import PluginServer
|
|
6
|
+
from insightconnect_plugin_runtime.server import PluginServer
|
|
7
7
|
from tests.plugin.hello_world import KomandHelloWorld
|
|
8
8
|
from .utils import MockResponse, Logger
|
|
9
9
|
|
|
@@ -18,26 +18,20 @@ class TestServerCloudPlugins(TestCase):
|
|
|
18
18
|
self.plugin_name = self.plugin.name.lower().replace(" ", "_")
|
|
19
19
|
|
|
20
20
|
@parameterized.expand([["Set cloud to false", False], ["Set cloud to true", True]])
|
|
21
|
-
@patch("insightconnect_plugin_runtime.server.PluginServer.register_scheduled_tasks")
|
|
22
21
|
@patch("insightconnect_plugin_runtime.server.request_get")
|
|
23
|
-
def test_cloud_plugin_no_tasks_ignore_cps(self, _test_name, cloud, mocked_req,
|
|
22
|
+
def test_cloud_plugin_no_tasks_ignore_cps(self, _test_name, cloud, mocked_req, mock_cloud, _run):
|
|
24
23
|
mock_cloud.return_value = cloud # Mock plugin running in cloud vs not
|
|
25
24
|
self.plugin.tasks = None # ensure still no tasks as other tests edit this and could fail before reverting
|
|
26
25
|
plugin_server = PluginServer(self.plugin) # this plugin has no tasks by default
|
|
27
26
|
|
|
28
27
|
plugin_server.start()
|
|
29
28
|
self.assertEqual(plugin_server.config_options, {})
|
|
30
|
-
self.assertEqual(plugin_server.schedule_interval, None)
|
|
31
|
-
|
|
32
|
-
# Depending on if we're running cloud or not this could be called but we only reach to CPS if we have tasks
|
|
33
|
-
self.assertEquals(mocked_scheduler.called, cloud)
|
|
34
29
|
|
|
35
30
|
# Plugin server never calls out to CPS as either we are not running in cloud mode or have no tasks.
|
|
36
31
|
self.assertFalse(mocked_req.called)
|
|
37
32
|
|
|
38
|
-
@patch("insightconnect_plugin_runtime.server.PluginServer.register_scheduled_tasks")
|
|
39
33
|
@patch("insightconnect_plugin_runtime.server.request_get")
|
|
40
|
-
def test_cloud_plugin_calls_cps(self, mocked_req,
|
|
34
|
+
def test_cloud_plugin_calls_cps(self, mocked_req, _mock_cloud, _run):
|
|
41
35
|
mocked_req.return_value = MockResponse({"plugins":{self.plugin_name: PLUGIN_VALUE_1, 'plugin': PLUGIN_VALUE_2},
|
|
42
36
|
"config": {"interval": 25}})
|
|
43
37
|
self.plugin.tasks = 'fake tasks' # this plugin by default has no tasks so force it to have some
|
|
@@ -49,17 +43,15 @@ class TestServerCloudPlugins(TestCase):
|
|
|
49
43
|
|
|
50
44
|
# We only save the plugin config for the current config and ignore `other_plugin`
|
|
51
45
|
self.assertDictEqual(plugin_server.config_options, PLUGIN_VALUE_1)
|
|
52
|
-
self.assertEqual(plugin_server.schedule_interval, SCHEDULE_INTERVAL)
|
|
53
46
|
|
|
54
|
-
# We should now schedule this to run
|
|
55
|
-
self.assertEquals(mocked_scheduler.called, True)
|
|
56
47
|
self.plugin.tasks = None # reset tasks value
|
|
57
48
|
|
|
58
49
|
@parameterized.expand([["error", HTTPError], ["error", Timeout], ["unexpected", TooManyRedirects]])
|
|
59
|
-
@patch("insightconnect_plugin_runtime.server.PluginServer.register_scheduled_tasks")
|
|
60
50
|
@patch("insightconnect_plugin_runtime.server.request_get")
|
|
61
51
|
@patch("structlog.get_logger")
|
|
62
|
-
|
|
52
|
+
@patch("insightconnect_plugin_runtime.server.CPS_RETRY", new=2) # reduce retries in unit tests
|
|
53
|
+
@patch("insightconnect_plugin_runtime.server.RETRY_SLEEP", new=1) # reduce sleep in unit tests
|
|
54
|
+
def test_cps_raises_an_error(self, test_cond, exception, log, mocked_req, _mock_cloud, _run):
|
|
63
55
|
log.return_value = Logger()
|
|
64
56
|
# If we have successfully got config and scheduler options, and later this call fails we should keep values
|
|
65
57
|
mocked_req.return_value = MockResponse({"plugins": {self.plugin_name: PLUGIN_VALUE_1, 'plugin': PLUGIN_VALUE_2},
|
|
@@ -70,9 +62,8 @@ class TestServerCloudPlugins(TestCase):
|
|
|
70
62
|
plugin_server.start()
|
|
71
63
|
|
|
72
64
|
self.assertDictEqual(plugin_server.config_options, PLUGIN_VALUE_2)
|
|
73
|
-
self.assertEqual(plugin_server.schedule_interval, DEFAULT_SCHEDULE_INTERVAL_MINUTES) # no resp uses default
|
|
74
65
|
|
|
75
|
-
# First call has happened and now successful - force
|
|
66
|
+
# First call has happened and now successful - force to hit specific handled and unexpected errors.
|
|
76
67
|
mocked_req.side_effect = exception("Warning HTTP error returned...")
|
|
77
68
|
plugin_server.get_plugin_properties_from_cps()
|
|
78
69
|
# we log error in all and `unexpected` in TooManyRedirects as there is no direct catch for this
|
|
@@ -80,16 +71,13 @@ class TestServerCloudPlugins(TestCase):
|
|
|
80
71
|
|
|
81
72
|
# Values should not have changed
|
|
82
73
|
self.assertDictEqual(plugin_server.config_options, PLUGIN_VALUE_2)
|
|
83
|
-
self.assertEqual(plugin_server.schedule_interval, DEFAULT_SCHEDULE_INTERVAL_MINUTES)
|
|
84
74
|
|
|
85
|
-
# Next schedule returns
|
|
86
|
-
|
|
87
|
-
mocked_req.return_value = MockResponse({"config": {"interval": new_schedule}})
|
|
75
|
+
# Next schedule returns no configurations for plugins
|
|
76
|
+
mocked_req.return_value = MockResponse({})
|
|
88
77
|
mocked_req.side_effect = None
|
|
89
78
|
plugin_server.get_plugin_properties_from_cps()
|
|
90
79
|
|
|
91
80
|
# And this new values are now updated for the plugin server
|
|
92
81
|
self.assertDictEqual(plugin_server.config_options, {})
|
|
93
|
-
self.assertEqual(plugin_server.schedule_interval, new_schedule)
|
|
94
82
|
|
|
95
83
|
self.plugin.tasks = None # reset tasks value
|
|
File without changes
|
|
File without changes
|