Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 3 additions & 0 deletions docs/core_lib_handler.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# core_lib_handler.py

::: solnlib.core_lib_handler
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ nav:
- "acl.py": acl.md
- "credentials.py": credentials.md
- "conf_manager.py": conf_manager.md
- "core_lib_handler.py": core_lib_handler.md
- "file_monitor.py": file_monitor.md
- "hec_config.py": hec_config.md
- "log.py": log.md
Expand Down
2 changes: 2 additions & 0 deletions solnlib/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
acl,
bulletin_rest_client,
conf_manager,
core_lib_handler,
credentials,
file_monitor,
hec_config,
Expand All @@ -40,6 +41,7 @@
"acl",
"bulletin_rest_client",
"conf_manager",
"core_lib_handler",
"credentials",
"file_monitor",
"hec_config",
Expand Down
133 changes: 133 additions & 0 deletions solnlib/core_lib_handler.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
#
# Copyright 2025 Splunk Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#

import os
import sys
import re
from types import ModuleType
import shutil
import importlib


def _is_module_from_splunk_core(lib_module: ModuleType) -> bool:
"""Check if the imported module is from the Splunk-provided libraries."""
core_site_packages_regex = _get_core_site_packages_regex()

splunk_site_packages_paths = [
path for path in sys.path if core_site_packages_regex.search(path)
]

return any(
_is_core_site_package_path(
splunk_site_packages_path, lib_module.__name__, lib_module.__file__
)
for splunk_site_packages_path in splunk_site_packages_paths
)


def _is_core_site_package_path(
core_site_packages_directory: str, module_name: str, module_path: str
) -> bool:
"""Check if the module path originates from a core site-packages
directory."""
return os.path.join(core_site_packages_directory, module_name) in module_path


def _get_core_site_packages_regex() -> re.Pattern:
"""Get the regex pattern for matching site-packages directories."""
sep = os.path.sep
sep_escaped = re.escape(sep)

return (
re.compile(
r"Python(?:-\d+(?:\.\d+)?)?"
+ sep_escaped
+ r"lib"
+ sep_escaped
+ r"site-packages$",
re.IGNORECASE,
)
if sys.platform.startswith("win32")
else re.compile(
r"lib"
+ r"("
+ sep_escaped
+ r"python\d+(\.\d+)?"
+ r")?"
+ sep_escaped
+ r"site-packages$"
)
)


def _cache_lib(lib_name: str):
"""Import the Splunk-shipped library first, before adding TA paths to
sys.path, to ensure it is cached.

This way, even if the TA path added to sys.path contains the
specified library, Python will always reference the already cached
library from the Splunk Python path.
"""
lib_module = importlib.import_module(lib_name)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm wondering if this way of importing wouldn't cause issue in the future if, for any reason, we'd like to override some library that comes with Splunk

I'd be more comfortable with something that just checks if there is a possibility to import rather than importing (eg. importlib.util.find_spec)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right now we're doing something opposite - we're overriding our libraries with the ones from Splunk. In this case if some lib does not exists, it is an error and should be found during testing

assert _is_module_from_splunk_core(
lib_module
), f"The module {lib_name} is not from Splunk core site-packages."


def _get_app_path(absolute_path: str, current_script_folder: str = "lib") -> str:
"""Returns app path."""
marker = os.path.join(os.path.sep, "etc", "apps")
start = absolute_path.rfind(marker)
if start == -1:
return None
end = absolute_path.find(current_script_folder, start)
if end == -1:
return None
end = end - 1
path = absolute_path[:end]
return path


def _remove_lib_folder(lib_name: str):
"""List and attempt to remove any folders directly under the 'lib'
directory that contain lib_name in their name.

Handles exceptions during removal, allowing the script to proceed
even if errors occur.
"""

app_dir = _get_app_path(os.path.abspath(__file__))
if app_dir is None:
print(f"WARNING: Unable to determine app directory path for {lib_name}")
return

lib_dir = os.path.join(app_dir, "lib")

try:
for entry in os.listdir(lib_dir):
entry_path = os.path.join(lib_dir, entry)
if os.path.isdir(entry_path) and lib_name in entry:
try:
shutil.rmtree(entry_path)
except Exception as e:
print(f"ERROR: Failed to remove library folder {entry_path}: {e}")
except Exception as e:
print(f"ERROR: Error in _remove_lib_folder for {lib_name}: {e}")


def handle_splunk_provided_lib(lib_name: str):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do believe that should be more meaningful what it does underneath - warning ... maybe even alerting - like: delete_addon_lib

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

delete is not the correct word here, as deleting is just a part of the job to be done. We should find a word for both importing new one and removing leftovers

_cache_lib(lib_name)
_remove_lib_folder(lib_name)
Loading
Loading