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.
@@ -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, self.schedule_interval = {}, None
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
- self.logger.info("Getting plugin configuration information...")
187
- try:
188
- request_response = request_get(CPS_ENDPOINT, timeout=30)
189
- resp_json = request_response.json()
190
- plugin = self.plugin.name.lower().replace(" ", "_") # match how we name our images
191
- plugin_config = resp_json.get("plugins", {}).get(plugin, {})
192
-
193
- self.config_options = plugin_config
194
- self.schedule_interval = resp_json.get("config", {}).get("interval", DEFAULT_SCHEDULE_INTERVAL_MINUTES)
195
- except MissingSchema as missing_schema:
196
- self.logger.error(f"Invalid URL being requested: {CPS_ENDPOINT}, error={missing_schema}")
197
- except Timeout as timeout:
198
- self.logger.error(f"Connection timeout hit. CPS={CPS_ENDPOINT}, error={timeout}")
199
- except HTTPError as http_error:
200
- self.logger.error(f"Connection error when trying to reach CPS. CPS={CPS_ENDPOINT}, error={http_error}")
201
- except JSONDecodeError as bad_json:
202
- self.logger.error(f"Got bad JSON back. Response content={request_response.content}, error={bad_json}")
203
- except Exception as error:
204
- self.logger.error(f"Hit an unexpected error when retrieving plugin custom configs, error={error}")
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.0
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 ![Build Status](https://github.com/rapid7/komand-plugin-sdk-python/workflows/Continuous%20Integration/badge.svg)
@@ -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 | Bookworm |
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=2TMIf5lXmYvDgQu2a6VOGcu6saJmcQoDTKfNNvyBj9E,12693
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=I4ejyCP8zK0H0QD8KG3LHuOlKRXFGORbL0BOrVqyzME,5515
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.0.dist-info/METADATA,sha256=uNV5HcyCbc49hSsQIY3UfJ2GnbB0ktGamEn03NcNJyA,12287
82
- insightconnect_plugin_runtime-5.4.0.dist-info/WHEEL,sha256=oiQVh_5PnQM0E3gPdiz09WCNmwiHDMaGer_elqB3coM,92
83
- insightconnect_plugin_runtime-5.4.0.dist-info/top_level.txt,sha256=AJtyJOpiFzHxsbHUICTcUKXyrGQ3tZxhrEHsPjJBvEA,36
84
- insightconnect_plugin_runtime-5.4.0.dist-info/RECORD,,
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, DEFAULT_SCHEDULE_INTERVAL_MINUTES
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, mocked_scheduler, mock_cloud, _run):
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, mocked_scheduler, _mock_cloud, _run):
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
- def test_cps_schedule_raises_an_error(self, test_cond, exception, log, mocked_req, _mocked_scheduler, _mock_cloud, _run):
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 scheduler to hit specific handled and unexpected errors.
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 updated values, new schedule and no configurations for plugins
86
- new_schedule = 12345678
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