Skip to content
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

feat: add support for custom REST handlers without UI in web.conf and restmap.conf #1532

Merged
merged 36 commits into from
Feb 18, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
1fb8f3b
REST handlers schema
kkedziak-splunk Dec 17, 2024
34e35b8
REST handlers schema
kkedziak-splunk Dec 17, 2024
f9ef414
Merge branch 'develop' into feat/handlers-custom-logic
kkedziak-splunk Jan 3, 2025
f60c99a
Merge branch 'refs/heads/develop' into feat/handlers-custom-logic
kkedziak-splunk Jan 7, 2025
6b32b8a
Merge branch 'refs/heads/develop' into feat/handlers-custom-logic
kkedziak-splunk Jan 14, 2025
ad6ff17
User defined handlers
kkedziak-splunk Jan 15, 2025
2f76a33
Change a smoke test
kkedziak-splunk Jan 15, 2025
ad7e9e8
Fix test
kkedziak-splunk Jan 15, 2025
849e407
License
kkedziak-splunk Jan 16, 2025
091b39b
Handler type
kkedziak-splunk Jan 16, 2025
d7b9824
Simplify
kkedziak-splunk Jan 16, 2025
4ae88c5
Change regex
kkedziak-splunk Jan 17, 2025
3710458
Add name param
kkedziak-splunk Jan 17, 2025
3eb1ce8
Docstrings
kkedziak-splunk Jan 17, 2025
8e8ad4f
Fix smoke test
kkedziak-splunk Jan 17, 2025
11abca9
User defined rest handlers - conf files
kkedziak-splunk Jan 20, 2025
bf105ee
Merge branch 'develop' into feat/handlers-custom-logic
kkedziak-splunk Jan 21, 2025
9321928
Merge branch 'feat/handlers-custom-logic' into feat/handlers-custom-l…
kkedziak-splunk Jan 21, 2025
df423b8
Fix for missing OAI params
kkedziak-splunk Feb 5, 2025
8b74069
Merge branch 'develop' into feat/handlers-custom-logic-2
kkedziak-splunk Feb 5, 2025
f183015
Merge branch 'develop' into feat/handlers-custom-logic
kkedziak-splunk Feb 5, 2025
fcb65aa
Merge branch 'develop' into feat/handlers-custom-logic
kkedziak-splunk Feb 7, 2025
fc8ee48
Merge branch 'feat/handlers-custom-logic' into feat/handlers-custom-l…
kkedziak-splunk Feb 7, 2025
8ca8213
Merge branch 'develop' into feat/handlers-custom-logic-2
kkedziak-splunk Feb 11, 2025
dfe17c4
Remove file validation
kkedziak-splunk Feb 11, 2025
f911a23
Merge branch 'develop' into feat/handlers-custom-logic-2
kkedziak-splunk Feb 17, 2025
c199115
Fix smoke
kkedziak-splunk Feb 17, 2025
598fb26
Merge branch 'develop' into feat/handlers-custom-logic-2
kkedziak-splunk Feb 17, 2025
9196596
Merge branch 'develop' into feat/handlers-custom-logic-2
kkedziak-splunk Feb 18, 2025
fe483bd
Revert file generator change
kkedziak-splunk Feb 18, 2025
e8c4611
More readable errors
kkedziak-splunk Feb 18, 2025
e4ca638
Remove the file check
kkedziak-splunk Feb 18, 2025
121e255
Merge branch 'develop' into feat/handlers-custom-logic-2
kkedziak-splunk Feb 18, 2025
dfab99b
Fix the test
kkedziak-splunk Feb 18, 2025
c0ee40b
Merge branch 'develop' into feat/handlers-custom-logic-2
kkedziak-splunk Feb 18, 2025
d650667
Merge branch 'develop' into feat/handlers-custom-logic-2
kkedziak-splunk Feb 18, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,23 @@ def _eai_response_schema(schema: Any) -> oas.MediaTypeObject:
)


@dataclass
class EndpointRegistrationEntry:
"""
Represents an entry in the endpoint registration file.
"""

name: str
rh_name: str
actions_list: List[str]

def actions(self) -> List[str]:
"""
Method for consistency with RestEndpointBuilder.
"""
return self.actions_list


@dataclass
class RestHandlerConfig:
"""
Expand Down Expand Up @@ -155,7 +172,7 @@ def _oas_object_eai_create_or_edit(
if action not in self.supported_actions:
return None

request_parameters = deepcopy(self.request_parameters[action])
request_parameters = deepcopy(self.request_parameters.get(action, {}))

if action == "create":
request_parameters["name"] = {
Expand Down Expand Up @@ -270,6 +287,27 @@ def oas_paths(self) -> Dict[str, oas.PathItemObject]:
else:
raise ValueError(f"Unsupported handler type: {self.handlerType}")

@property
def endpoint_registration_entry(self) -> Optional[EndpointRegistrationEntry]:
if not self.registerHandler:
return None

if not self.registerHandler.get("actions") or not self.registerHandler.get(
"file"
):
return None

file: str = self.registerHandler["file"]

if file.endswith(".py"):
file = file[:-3]

return EndpointRegistrationEntry(
name=self.endpoint,
rh_name=file,
actions_list=self.registerHandler["actions"],
)


class UserDefinedRestHandlers:
"""
Expand All @@ -294,10 +332,16 @@ def add_definition(
definition = RestHandlerConfig(**definition)

if definition.name in self._names:
raise ValueError(f"Duplicate REST handler name: {definition.name}")
raise ValueError(
f"REST handler defined in Global Config contains duplicated name: {definition.name}. "
"Please change it to a unique name."
)

if definition.endpoint in self._endpoints:
raise ValueError(f"Duplicate REST handler endpoint: {definition.endpoint}")
raise ValueError(
f"REST handler defined in Global Config contains duplicated endpoint: {definition.endpoint} "
f"(name={definition.name}). Please change it to a unique endpoint."
)

self._names.add(definition.name)
self._endpoints.add(definition.endpoint)
Expand All @@ -312,3 +356,17 @@ def oas_paths(self) -> Dict[str, oas.PathItemObject]:
paths.update(definition.oas_paths)

return paths

@property
def endpoint_registration_entries(self) -> List[EndpointRegistrationEntry]:
entries = []

for definition in self._definitions:
entry = definition.endpoint_registration_entry

if entry is None:
continue

entries.append(entry)

return entries
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,14 @@
# See the License for the specific language governing permissions and
# limitations under the License.
#
from typing import Any, Dict, Union
from typing import Any, Dict, Union, List

from splunk_add_on_ucc_framework.commands.rest_builder.endpoint.base import (
RestEndpointBuilder,
)
from splunk_add_on_ucc_framework.commands.rest_builder.user_defined_rest_handlers import (
EndpointRegistrationEntry,
)
from splunk_add_on_ucc_framework.generators.conf_files import ConfGenerator


Expand All @@ -26,10 +32,16 @@ class RestMapConf(ConfGenerator):

def _set_attributes(self, **kwargs: Any) -> None:
self.conf_file = "restmap.conf"
self.endpoints: List[Union[RestEndpointBuilder, EndpointRegistrationEntry]] = []

if self._global_config and self._global_config.has_pages() and self._gc_schema:
self.endpoints = self._gc_schema.endpoints
self.endpoint_names = ", ".join(sorted([ep.name for ep in self.endpoints]))
self.endpoints.extend(self._gc_schema.endpoints)
self.namespace = self._gc_schema.namespace
self.endpoints.extend(
self._global_config.user_defined_handlers.endpoint_registration_entries
)

self.endpoint_names = ", ".join(sorted([ep.name for ep in self.endpoints]))

def generate_conf(self) -> Union[Dict[str, str], None]:
if not (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,14 @@
# See the License for the specific language governing permissions and
# limitations under the License.
#
from typing import Any, Dict, Union
from typing import Any, Dict, Union, List

from splunk_add_on_ucc_framework.commands.rest_builder.endpoint.base import (
RestEndpointBuilder,
)
from splunk_add_on_ucc_framework.commands.rest_builder.user_defined_rest_handlers import (
EndpointRegistrationEntry,
)
from splunk_add_on_ucc_framework.generators.conf_files import ConfGenerator


Expand All @@ -26,8 +32,13 @@ class WebConf(ConfGenerator):

def _set_attributes(self, **kwargs: Any) -> None:
self.conf_file = "web.conf"
self.endpoints: List[Union[RestEndpointBuilder, EndpointRegistrationEntry]] = []

if self._global_config and self._global_config.has_pages() and self._gc_schema:
self.endpoints = self._gc_schema.endpoints
self.endpoints.extend(self._gc_schema.endpoints)
self.endpoints.extend(
self._global_config.user_defined_handlers.endpoint_registration_entries
)

def generate_conf(self) -> Union[Dict[str, str], None]:
if not (
Expand Down
7 changes: 5 additions & 2 deletions splunk_add_on_ucc_framework/schema/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -2618,15 +2618,18 @@
"properties": {
"file": {
"type": "string",
"description": "The file where the custom rest handler is located."
"description": "The file where the custom rest handler is located.",
"pattern": "^[a-zA-Z0-9_]+\\.py$"
},
"actions": {
"type": "array",
"description": "The actions that the custom rest handler supports.",
"items": {
"type": "string",
"description": "The action that the custom rest handler supports.",
"enum": ["list", "edit", "create", "remove"]
"enum": ["list", "edit", "create", "remove"],
"minItems": 1,
"uniqueItems": true
}
}
},
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[admin:splunk_ta_uccexample]
match = /
members = splunk_ta_uccexample_account, splunk_ta_uccexample_oauth, splunk_ta_uccexample_settings
members = Splunk_TA_Example_full, splunk_ta_uccexample_account, splunk_ta_uccexample_oauth, splunk_ta_uccexample_settings

[admin_external:splunk_ta_uccexample_account]
handlertype = python
Expand All @@ -21,4 +21,11 @@ handlertype = python
python.version = python3
handlerfile = splunk_ta_uccexample_rh_settings.py
handleractions = edit, list
handlerpersistentmode = true

[admin_external:Splunk_TA_Example_full]
handlertype = python
python.version = python3
handlerfile = someFile.py
handleractions = create, edit, list
handlerpersistentmode = true
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,12 @@ methods = POST, GET

[expose:splunk_ta_uccexample_settings_specified]
pattern = splunk_ta_uccexample_settings/*
methods = POST, GET, DELETE

[expose:Splunk_TA_Example_full]
pattern = Splunk_TA_Example_full
methods = POST, GET

[expose:Splunk_TA_Example_full_specified]
pattern = Splunk_TA_Example_full/*
methods = POST, GET, DELETE
Original file line number Diff line number Diff line change
Expand Up @@ -393,6 +393,14 @@
"name": "rest_handler_full",
"endpoint": "Splunk_TA_Example_full",
"handlerType": "EAI",
"registerHandler": {
"file": "someFile.py",
"actions": [
"create",
"edit",
"list"
]
},
"requestParameters": {
"create": {
"param1_req": {
Expand Down
129 changes: 129 additions & 0 deletions tests/unit/generators/conf_files/test_create_restmap_conf.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,21 @@
import os.path
from textwrap import dedent

from pytest import fixture
from unittest.mock import patch, MagicMock

from splunk_add_on_ucc_framework import __file__ as ucc_framework_file
from splunk_add_on_ucc_framework.commands.rest_builder.user_defined_rest_handlers import (
RestHandlerConfig,
)
from splunk_add_on_ucc_framework.generators.conf_files import RestMapConf
from splunk_add_on_ucc_framework.global_config import GlobalConfig
from tests.unit.helpers import get_testdata_file_path


UCC_DIR = os.path.dirname(ucc_framework_file)


@fixture
def global_config():
return GlobalConfig(get_testdata_file_path("valid_config.json"))
Expand Down Expand Up @@ -106,3 +117,121 @@ def test_set_attributes(global_config, input_dir, output_dir, ucc_dir, ta_name):
assert hasattr(restmap_conf, "endpoints")
assert hasattr(restmap_conf, "endpoint_names")
assert hasattr(restmap_conf, "namespace")


def test_restmap_endpoints(global_config, input_dir, output_dir, ta_name):
expected_top = (
"[admin:splunk_ta_uccexample]\n"
"match = /\n"
"members = splunk_ta_uccexample_account, splunk_ta_uccexample_example_input_one, "
"splunk_ta_uccexample_example_input_two, splunk_ta_uccexample_oauth, splunk_ta_uccexample_settings\n\n"
)

expected_content = dedent(
"""
[admin_external:splunk_ta_uccexample_account]
handlertype = python
python.version = python3
handlerfile = splunk_ta_uccexample_rh_account.py
handleractions = edit, list, remove, create
handlerpersistentmode = true

[admin_external:splunk_ta_uccexample_oauth]
handlertype = python
python.version = python3
handlerfile = splunk_ta_uccexample_rh_oauth.py
handleractions = edit
handlerpersistentmode = true

[admin_external:splunk_ta_uccexample_settings]
handlertype = python
python.version = python3
handlerfile = splunk_ta_uccexample_rh_settings.py
handleractions = edit, list
handlerpersistentmode = true

[admin_external:splunk_ta_uccexample_example_input_one]
handlertype = python
python.version = python3
handlerfile = splunk_ta_uccexample_rh_example_input_one.py
handleractions = edit, list, remove, create
handlerpersistentmode = true

[admin_external:splunk_ta_uccexample_example_input_two]
handlertype = python
python.version = python3
handlerfile = splunk_ta_uccexample_rh_example_input_two.py
handleractions = edit, list, remove, create
handlerpersistentmode = true
"""
).lstrip()
restmap_conf = RestMapConf(
global_config, input_dir, output_dir, addon_name=ta_name, ucc_dir=UCC_DIR
)
file_paths = restmap_conf.generate_conf()

assert file_paths is not None
assert file_paths.keys() == {"restmap.conf"}

with open(file_paths["restmap.conf"]) as fp:
content = fp.read()

assert content == (expected_top + expected_content)

global_config.user_defined_handlers.add_definitions(
[
RestHandlerConfig(
name="name1",
endpoint="endpoint1",
handlerType="EAI",
registerHandler={"file": "file1", "actions": ["list"]},
),
RestHandlerConfig(
name="name2",
endpoint="endpoint2",
handlerType="EAI",
registerHandler={
"file": "file2",
"actions": ["list", "create", "delete", "edit"],
},
),
RestHandlerConfig(
name="name3",
endpoint="endpoint3",
handlerType="EAI",
),
]
)

restmap_conf = RestMapConf(
global_config, input_dir, output_dir, addon_name=ta_name, ucc_dir=UCC_DIR
)
file_paths = restmap_conf.generate_conf()

assert file_paths is not None
assert file_paths.keys() == {"restmap.conf"}

with open(file_paths["restmap.conf"]) as fp:
content = fp.read()

expected_top = expected_top.replace("members =", "members = endpoint1, endpoint2,")

expected_content += dedent(
"""
[admin_external:endpoint1]
handlertype = python
python.version = python3
handlerfile = file1.py
handleractions = list
handlerpersistentmode = true

[admin_external:endpoint2]
handlertype = python
python.version = python3
handlerfile = file2.py
handleractions = list, create, delete, edit
handlerpersistentmode = true
"""
)

assert content == expected_top + expected_content
Loading
Loading