insightconnect-plugin-runtime 6.2.6__py3-none-any.whl → 6.3.1__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.
@@ -24,9 +24,11 @@ from insightconnect_plugin_runtime.exceptions import (
24
24
  ServerException,
25
25
  )
26
26
  from insightconnect_plugin_runtime.util import OutputMasker
27
+ from uuid import UUID
27
28
 
28
29
  logger = structlog.get_logger("plugin")
29
30
  ORG_ID = "X-IPIMS-ORGID"
31
+ INT_ID = "X-INTEGRATION-ID"
30
32
 
31
33
  PLUGIN_SPEC_DOCKERFILE = "/python/src/plugin.spec.yaml"
32
34
  PLUGIN_SPEC_PACK = "/workspace/plugin.spec.yaml"
@@ -173,15 +175,20 @@ class Endpoints:
173
175
  500:
174
176
  description: Unexpected error
175
177
  """
178
+
176
179
  self.logger.info("Plugin task beginning execution...")
177
180
  input_message = request.get_json(force=True)
178
181
  self.logger.debug("Request input: %s", input_message)
179
182
  Endpoints.validate_action_trigger_task_empty_input(input_message)
180
183
  Endpoints.validate_action_trigger_task_name(input_message, name, "task")
184
+
181
185
  # No validation on the plugin custom config to leave this as configurable as possible.
182
186
  # `add_plugin_custom_config` will pass any available values to the plugin for interpretation.
183
187
  input_message = self.add_plugin_custom_config(
184
- input_message, request.headers.get(ORG_ID)
188
+ input_message,
189
+ request.headers.get(ORG_ID, ""),
190
+ request.headers.get(INT_ID, ""),
191
+ name,
185
192
  )
186
193
  output = self.run_action_trigger_task(input_message, mask_output=False)
187
194
  self.logger.info("Plugin task finished execution...")
@@ -787,7 +794,7 @@ class Endpoints:
787
794
  return version
788
795
 
789
796
  def add_plugin_custom_config(
790
- self, input_data: Dict[str, Any], org_id: str
797
+ self, input_data: Dict[str, Any], org_id: str, int_id: str, task_name: str
791
798
  ) -> Dict[str, Any]:
792
799
  """
793
800
  Using the retrieved configs pulled from komand-props, pass the configuration that matches the requesting
@@ -800,17 +807,95 @@ class Endpoints:
800
807
  - org 1 when in lookback mode will pull back 108 hours (task triggered with no state).
801
808
  - all orgs for default runs will poll back 12 hours.
802
809
  - all orgs in lookback mode will be 100 hours (task triggered with no state).
810
+
811
+ When Int ID is specified or task name. Then it can be used across all orgs (global config) or specific Org ID.
812
+ There's a hierarchy between task related config and Int ID related, where Int ID will always
813
+ replace task related config due to its higher priority.
814
+
815
+ Config example:
816
+ {
817
+ "org_1": {
818
+ "task_name_1": {
819
+ "default": 12,
820
+ "lookback": "100"
821
+ },
822
+ "int_1": {
823
+ "default": 6,
824
+ "lookback": "50"
825
+ },
826
+ {
827
+ "default": 24,
828
+ "lookback": "108"
829
+ }
830
+ },
831
+ "*": {
832
+ "default": 24,
833
+ "lookback": 200
834
+ "task_name_2": {
835
+ "default": 2,
836
+ "lookback": "10"
837
+ }
838
+ }
839
+ }
840
+
841
+ In this config example the following we be applied:
842
+ - org 1 to has a custom default time of 24 hours for their timings.
843
+ - org 1 when in lookback mode will pull back 108 hours (task triggered with no state).
844
+ - org 1 all integrations that uses task with 'task_name_1' will pull back 100 hours
845
+ and have a custom default time of 12 hours for their timings.
846
+ - org 1 specific integration 'int_1' will pull back 50 hours and have a custom
847
+ default time of 6 hours for their timings.
848
+ - all orgs for default runs will poll back 24 hours.
849
+ - all orgs in lookback mode will be 200 hours (task triggered with no state).
850
+ - all orgs that uses task with 'task_name_2' will pull back 2 hours
851
+ - all orgs that uses task with 'task_name_2' in lookback mode will be 10 hours
803
852
  """
804
- additional_config = self.config_options.get(org_id) or self.config_options.get(
805
- "*"
806
- )
853
+
854
+ # Parse configuration based on organization or global. Use its copies, not to modify original dict.
855
+ organization_config, global_config = self.config_options.get(org_id, {}).copy(), self.config_options.get("*", {}).copy()
856
+
857
+ # Definition of additional config and its type variables.
858
+ additional_config, config_type = {}, ""
859
+
860
+ # Check if we have a global config.
861
+ # Also, use "config_type" variable just for logging purposes to have an indicator from where the config was pulled.
862
+ if global_config:
863
+ # Setup global config as starting point.
864
+ additional_config, config_type = global_config, "GLOBAL"
865
+
866
+ # If task configuration was found under global config, update it with task config to replace necessary values.
867
+ if task_config := global_config.get(task_name):
868
+ additional_config.update(task_config)
869
+ config_type = "GLOBAL_TASK"
870
+
871
+ # If organization config is present, then replace its values with the ones coming from global config.
872
+ if organization_config:
873
+ # Update additional config with values coming from organization config.
874
+ additional_config.update(organization_config)
875
+ config_type = "ORG"
876
+
877
+ # If task configuration was found under organization, replace its values with organization config.
878
+ if task_config := organization_config.get(task_name):
879
+ additional_config.update(task_config)
880
+ config_type = "ORG_TASK"
881
+
882
+ # If integration config was found under organization replace its values with organization
883
+ # and task config (higher priority).
884
+ if integration_config := organization_config.get(int_id):
885
+ additional_config.update(integration_config)
886
+ config_type = "ORG_INT"
887
+
807
888
  if additional_config:
808
889
  self.logger.info(
809
- "Found config options; adding this to the request parameters..."
890
+ f"Found config options ({config_type}); adding this to the request parameters..."
810
891
  )
811
- additional_config = (
812
- additional_config.copy()
813
- ) # copy to preserve the referenced value in self.config_options
892
+
893
+ # Sopy to preserve the referenced value in self.config_options.
894
+ # Also, remove unnecessary fields from that config (int_ids, or task_names) that occurs
895
+ # when updated global dictionary above. This thing is that we use global config as base
896
+ # and then updating some fields depending on the configuration.
897
+ additional_config = self._remove_unnecessary_fields_from_custom_config(additional_config)
898
+
814
899
  # As a safeguard we only pass the lookback config params if the plugin has no state
815
900
  # This means we still need to manually delete the state for plugins on a per org basis.
816
901
  # This also means first time customers for their 'initial' lookup would get the lookback value passed in.
@@ -878,3 +963,46 @@ class Endpoints:
878
963
  return 501
879
964
  else:
880
965
  return 500
966
+
967
+ def _remove_unnecessary_fields_from_custom_config(self, additional_config: Dict[str, Any]) -> Dict[str, Any]:
968
+ """
969
+ Removes unnecessary fields from custom config such as other task names, and int_ids,
970
+ leaving only the fields that needs to be parsed.
971
+
972
+ :param additional_config: The custom config dictionary on which, unnecessary fields will be removed.
973
+ :type additional_config: Dict[str, Any]
974
+
975
+ :return: New custom config dictionary with unnecessary fields removed.
976
+ :rtype: Dict[str, Any]
977
+ """
978
+
979
+ # Copy 'additional_config' not to operate on it
980
+ config_copy = additional_config.copy()
981
+
982
+ # Remove other task names from config
983
+ for task_ in self.plugin.tasks.keys():
984
+ config_copy.pop(task_, None)
985
+
986
+ # Remove other int_ids from config
987
+ for key_ in additional_config.keys():
988
+ if self._check_if_uuid(key_):
989
+ config_copy.pop(key_, None)
990
+ return config_copy
991
+
992
+ @staticmethod
993
+ def _check_if_uuid(input_string: str) -> bool:
994
+ """
995
+ Validates whether the provided string matches UUID format specifications.
996
+
997
+ :param input_string: The string to validate against UUID format standards
998
+ :type input_string: str
999
+
1000
+ :return: True if the string is a valid UUID, False otherwise
1001
+ :rtype: bool
1002
+ """
1003
+
1004
+ try:
1005
+ UUID(input_string)
1006
+ return True
1007
+ except (TypeError, ValueError):
1008
+ return False
@@ -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
- self,
76
- plugin,
77
- port=10001,
78
- workers=1,
79
- threads=4,
80
- debug=False,
81
- worker_class="sync",
82
- worker_connections=200,
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() and self.plugin.tasks:
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.config_options = plugin_config
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
@@ -10,6 +10,7 @@ import python_jsonschema_objects as pjs
10
10
  KEYS_TO_CHECK_SECRETS = ("secretKey", "password", "key", "token")
11
11
  DEFAULT_REPLACE_THRESHOLD = 0.8
12
12
  DEFAULT_NUMBER_OF_ITERATIONS = 50
13
+ OTEL_ENDPOINT = "otel_tracing"
13
14
 
14
15
 
15
16
  class OutputMasker:
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.2
1
+ Metadata-Version: 2.4
2
2
  Name: insightconnect-plugin-runtime
3
- Version: 6.2.6
3
+ Version: 6.3.1
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,8 @@ 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.1 - Improved filtering for `custom_config` parameters for plugin tasks
228
+ * 6.3.0 - Add Tracing Instrumentation
222
229
  * 6.2.6 - Remove setuptools after installation
223
230
  * 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
231
  * 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=09fxsbKf2ZZvSqRP2Bv9e9-fspDyEFR8_YgIFeMnXqQ,12578
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=qPkZ3LA55nYuNYdansEbnCnBccQkpzIpp9NA1B64Kvw,8444
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=8QQrxzW8jmQIkalud8fqYwB05uUw8sTiDNgO5ZekOCA,33353
19
+ insightconnect_plugin_runtime/api/endpoints.py,sha256=ddqqYM7s1huvXFD0nCjpV_J2XULDn8F5sRakdjq-AKU,38893
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
@@ -66,19 +67,19 @@ tests/unit/test_action.py,sha256=0SVen1qNrFLKKAXMurN9T4NpItpftnSpDZ71AdrGZeo,168
66
67
  tests/unit/test_api.py,sha256=uZ1dWMmgQ-ZePYjmcZfjc-qOTJsjs20Wic0Uf4U12-8,4441
67
68
  tests/unit/test_aws_action.py,sha256=pBE23Qn4aXKJqPmwiHMcEU5zPdyvbKO-eK-6jUlrsQw,9640
68
69
  tests/unit/test_custom_encoder.py,sha256=KLYyVOTq9MEkZXyhVHqjm5LVSW6uJS4Davgghsw9DGk,2207
69
- tests/unit/test_endpoints.py,sha256=LuXOfLBu47rDjGa5YEsOwTZBEdvQdl_C6-r46oxWZA8,6401
70
+ tests/unit/test_endpoints.py,sha256=Ef0f6EudnAA_o6jZJ7rvgwgQHHswPuBEiyyo1D7BWMc,11523
70
71
  tests/unit/test_exceptions.py,sha256=Y4F-ij8WkEJkUU3mPvxlEchqE9NCdxDvR8bJzPVVNao,5328
71
72
  tests/unit/test_helpers.py,sha256=ym1tFi1VSKmdPaHEAlMEl1S7Ibu9-LrqZ2oqJv7bfbE,18685
72
73
  tests/unit/test_metrics.py,sha256=PjjTrB9w7uQ2Q5UN-893-SsH3EGJuBseOMHSD1I004s,7979
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=PuMDHTz3af6lR9QK1BtPScr7_cRbWhetowADieVlXdo,5096
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.2.6.dist-info/METADATA,sha256=EAB_1cINrkNbkoHu5n6xbhtqrT6HwPbBonogpA_eqsE,15934
82
- insightconnect_plugin_runtime-6.2.6.dist-info/WHEEL,sha256=52BFRY2Up02UkjOa29eZOS2VxUrpPORXg1pkohGGUS8,91
83
- insightconnect_plugin_runtime-6.2.6.dist-info/top_level.txt,sha256=AJtyJOpiFzHxsbHUICTcUKXyrGQ3tZxhrEHsPjJBvEA,36
84
- insightconnect_plugin_runtime-6.2.6.dist-info/RECORD,,
82
+ insightconnect_plugin_runtime-6.3.1.dist-info/METADATA,sha256=_5pIsyrEG7UPyrY3bQdIxZtnq_7J9cdoRw7HGkWLWDo,16302
83
+ insightconnect_plugin_runtime-6.3.1.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
84
+ insightconnect_plugin_runtime-6.3.1.dist-info/top_level.txt,sha256=AJtyJOpiFzHxsbHUICTcUKXyrGQ3tZxhrEHsPjJBvEA,36
85
+ insightconnect_plugin_runtime-6.3.1.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (76.0.0)
2
+ Generator: setuptools (78.1.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -1,11 +1,42 @@
1
- import unittest
2
1
  import json
2
+ import unittest
3
+ from typing import Any, Dict
3
4
 
4
- from insightconnect_plugin_runtime.api.endpoints import Endpoints
5
- from insightconnect_plugin_runtime.plugin import Plugin
6
- from insightconnect_plugin_runtime.action import Action
7
5
  from insightconnect_plugin_runtime import Input
6
+ from insightconnect_plugin_runtime.action import Action
7
+ from insightconnect_plugin_runtime.api.endpoints import Endpoints
8
8
  from insightconnect_plugin_runtime.connection import Connection
9
+ from insightconnect_plugin_runtime.plugin import Plugin
10
+ from insightconnect_plugin_runtime.task import Task
11
+ from parameterized import parameterized
12
+
13
+ MOCKED_CONFIG = {
14
+ "*": {
15
+ "first_property": "first_property_global",
16
+ "second_property": "second_property_global",
17
+ "task_name_1": {"second_property": "second_property_global_task_name_1"},
18
+ },
19
+ "org_1": {
20
+ "first_property": "first_property_org_1",
21
+ "11111111-1111-1111-1111-111111111111": {
22
+ "first_property": "first_property_org_1_11111111-1111-1111-1111-111111111111"
23
+ },
24
+ "task_name_2": {"second_property": "second_property_task_name_2"},
25
+ "task_name_3": {
26
+ "first_property": "first_property_task_name_3",
27
+ "second_property": "second_property_task_name_3",
28
+ },
29
+ "22222222-2222-2222-2222-222222222222": {
30
+ "first_property": "first_property_org_1_22222222-2222-2222-2222-222222222222",
31
+ "second_property": "second_property_org_1_22222222-2222-2222-2222-222222222222",
32
+ },
33
+ },
34
+ "org_2": {
35
+ "first_property": "first_property_org_2",
36
+ "second_property": "second_property_org_2",
37
+ "task_name_1": {"first_property": "first_property_org_1_task_name_1"},
38
+ },
39
+ }
9
40
 
10
41
 
11
42
  class TestDefinitionsAllActions(unittest.TestCase):
@@ -18,6 +49,7 @@ class TestDefinitionsAllActions(unittest.TestCase):
18
49
  workers=None,
19
50
  threads=None,
20
51
  master_pid=None,
52
+ config_options=MOCKED_CONFIG,
21
53
  )
22
54
 
23
55
  plugin = Plugin(
@@ -28,6 +60,11 @@ class TestDefinitionsAllActions(unittest.TestCase):
28
60
  connection=Connection(input=None),
29
61
  )
30
62
 
63
+ # Add example tasks
64
+ for task in ("task_name_1", "task_name_2", "task_name_3"):
65
+ plugin.add_task(
66
+ Task(name=task, description="Test", input=None, output=None)
67
+ )
31
68
  self.endpoints.plugin = plugin
32
69
 
33
70
  def test_input_good(self):
@@ -213,3 +250,106 @@ class TestDefinitionsAllActions(unittest.TestCase):
213
250
  actual = self.endpoints._create_action_definitions_payload()
214
251
 
215
252
  self.assertNotEqual(expected, actual)
253
+
254
+ @parameterized.expand(
255
+ [
256
+ (
257
+ "",
258
+ "",
259
+ "no_task_in_properties",
260
+ {
261
+ "first_property": "first_property_global",
262
+ "second_property": "second_property_global",
263
+ },
264
+ ),
265
+ (
266
+ "",
267
+ "",
268
+ "task_name_1",
269
+ {
270
+ "first_property": "first_property_global",
271
+ "second_property": "second_property_global_task_name_1",
272
+ },
273
+ ),
274
+ (
275
+ "org_1",
276
+ "",
277
+ "no_task_in_properties",
278
+ {
279
+ "first_property": "first_property_org_1",
280
+ "second_property": "second_property_global",
281
+ },
282
+ ),
283
+ (
284
+ "org_1",
285
+ "11111111-1111-1111-1111-111111111111",
286
+ "no_task_in_properties",
287
+ {
288
+ "first_property": "first_property_org_1_11111111-1111-1111-1111-111111111111",
289
+ "second_property": "second_property_global",
290
+ },
291
+ ),
292
+ (
293
+ "org_1",
294
+ "11111111-1111-1111-1111-111111111111",
295
+ "task_name_2",
296
+ {
297
+ "first_property": "first_property_org_1_11111111-1111-1111-1111-111111111111",
298
+ "second_property": "second_property_task_name_2",
299
+ },
300
+ ),
301
+ (
302
+ "org_1",
303
+ "11111111-1111-1111-1111-111111111111",
304
+ "task_name_3",
305
+ {
306
+ "first_property": "first_property_org_1_11111111-1111-1111-1111-111111111111",
307
+ "second_property": "second_property_task_name_3",
308
+ },
309
+ ),
310
+ (
311
+ "org_1",
312
+ "22222222-2222-2222-2222-222222222222",
313
+ "no_task_in_properties",
314
+ {
315
+ "first_property": "first_property_org_1_22222222-2222-2222-2222-222222222222",
316
+ "second_property": "second_property_org_1_22222222-2222-2222-2222-222222222222",
317
+ },
318
+ ),
319
+ (
320
+ "org_1",
321
+ "22222222-2222-2222-2222-222222222222",
322
+ "task_name_2",
323
+ {
324
+ "first_property": "first_property_org_1_22222222-2222-2222-2222-222222222222",
325
+ "second_property": "second_property_org_1_22222222-2222-2222-2222-222222222222",
326
+ },
327
+ ),
328
+ (
329
+ "org_2",
330
+ "",
331
+ "",
332
+ {
333
+ "first_property": "first_property_org_2",
334
+ "second_property": "second_property_org_2",
335
+ },
336
+ ),
337
+ (
338
+ "org_2",
339
+ "",
340
+ "task_name_1",
341
+ {
342
+ "first_property": "first_property_org_1_task_name_1",
343
+ "second_property": "second_property_org_2",
344
+ },
345
+ ),
346
+ ]
347
+ )
348
+ def test_add_plugin_custom_config(
349
+ self, org_id: str, int_id: str, task_name: str, expected: Dict[str, Any]
350
+ ) -> None:
351
+ response = self.endpoints.add_plugin_custom_config(
352
+ {"body": {}}, org_id, int_id, task_name
353
+ )
354
+ custom_config = response.get("body", {}).get("custom_config", {})
355
+ self.assertEqual(custom_config, expected)
@@ -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
- # Plugin server never calls out to CPS as either we are not running in cloud mode or have no tasks.
31
- self.assertFalse(mocked_req.called)
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):