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
2 changes: 1 addition & 1 deletion bk-plugin-framework/bk_plugin_framework/__version__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,4 @@
specific language governing permissions and limitations under the License.
"""

__version__ = "2.3.2"
__version__ = "2.4.0rc0"
2 changes: 2 additions & 0 deletions bk-plugin-framework/bk_plugin_framework/kit/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,6 @@
State,
Callback,
FormModel,
Credential,
CredentialModel,
)
59 changes: 59 additions & 0 deletions bk-plugin-framework/bk_plugin_framework/kit/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,26 @@ class ContextRequire(BaseModel):
pass


class CredentialModel:
"""凭证模型基类,用于声明插件需要的凭证"""

pass


class Credential(BaseModel):
"""凭证定义类,用于声明插件需要的凭证"""

key: str
name: str = ""
description: str = ""

def __init__(self, key: str, name: str = "", description: str = "", **kwargs):
# 如果 name 为空,使用 key 作为 name
if not name:
name = key
super().__init__(key=key, name=name, description=description, **kwargs)


class Callback(object):
def __init__(self, callback_id: str = "", callback_data: dict = {}):
self.id = callback_id
Expand All @@ -66,6 +86,7 @@ def __init__(
callback: Callback = None,
outputs: typing.Optional[dict] = None,
storage: typing.Optional[dict] = None,
credentials: typing.Optional[dict] = None,
):
self.trace_id = trace_id
self.data = data
Expand All @@ -74,6 +95,7 @@ def __init__(
self.callback = callback
self.storage = storage or {}
self.outputs = outputs or {}
self.credentials = credentials or {}

@property
def schedule_context(self) -> dict:
Expand Down Expand Up @@ -140,6 +162,28 @@ def __new__(cls, name, bases, dct):
"plugin deinition error, {}'s ContextInputs is not subclass of {}".format(new_cls, ContextRequire)
)

# credentials validation (class attribute, similar to ContextInputs)
credentials_cls = getattr(new_cls, "Credentials", None)
if credentials_cls:
# Check if Credentials class inherits from CredentialModel
if not issubclass(credentials_cls, CredentialModel):
raise TypeError(
"plugin deinition error, {}'s Credentials is not subclass of {}".format(new_cls, CredentialModel)
)

# Validate each Credential instance in the Credentials class
for attr_name in dir(credentials_cls):
if attr_name.startswith("_"):
continue
attr_value = getattr(credentials_cls, attr_name)
if isinstance(attr_value, Credential):
if not attr_value.key:
raise ValueError(
"plugin deinition error, Credentials.{}.key cannot be empty in {}".format(
attr_name, new_cls
)
)

# inputs form check
inputs_form_cls = getattr(new_cls, "InputsForm", None)
if inputs_form_cls and not issubclass(inputs_form_cls, FormModel):
Expand Down Expand Up @@ -200,6 +244,7 @@ def dict(cls) -> dict:
"desc": getattr(cls.Meta, "desc", ""),
"version": cls.Meta.version,
"enable_plugin_callback": getattr(cls.Meta, "enable_plugin_callback", False),
"credentials": cls._EMPTY_SCHEMA,
"inputs": cls._EMPTY_SCHEMA,
"outputs": cls._EMPTY_SCHEMA,
"context_inputs": cls._EMPTY_SCHEMA,
Expand Down Expand Up @@ -227,4 +272,18 @@ def dict(cls) -> dict:
if context_cls:
data["context_inputs"] = cls._trim_schema(context_cls.schema())

# Extract credentials from Credentials class
credentials_cls = getattr(cls, "Credentials", None)
if credentials_cls:
credentials_list = []
for attr_name in dir(credentials_cls):
if attr_name.startswith("_"):
continue
attr_value = getattr(credentials_cls, attr_name)
if isinstance(attr_value, Credential):
credentials_list.append(attr_value)

data["credentials"] = [
{"key": c.key, "name": c.name, "description": c.description} for c in credentials_list
]
return data
10 changes: 6 additions & 4 deletions bk-plugin-framework/bk_plugin_framework/metrics.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,14 +129,16 @@ def _wrapper(*args, **kwargs):

# 正在执行的 EXECUTE 数量
BK_PLUGIN_EXECUTE_RUNNING_PROCESSES = Gauge(
name="bk_plugin_execute_running_processes", documentation="count running state processes",
labelnames=["hostname", "version"]
name="bk_plugin_execute_running_processes",
documentation="count running state processes",
labelnames=["hostname", "version"],
)

# 正在执行的 SCHEDULE 数量
BK_PLUGIN_SCHEDULE_RUNNING_PROCESSES = Gauge(
name="bk_plugin_schedule_running_processes", documentation="count running state schedules",
labelnames=["hostname", "version"]
name="bk_plugin_schedule_running_processes",
documentation="count running state schedules",
labelnames=["hostname", "version"],
)

# CALLBACK 异常次数
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,4 +80,5 @@ def callback(trace_id: str, callback_id: str, callback_data: str):
_set_schedule_state(trace_id=trace_id, state=State.FAIL)

BK_PLUGIN_CALLBACK_TIME.labels(hostname=HOSTNAME, version=schedule.plugin_version).observe(
time.perf_counter() - start)
time.perf_counter() - start
)
22 changes: 18 additions & 4 deletions bk-plugin-framework/bk_plugin_framework/runtime/executor.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,11 @@ def _plugin_finish_callback(self, plugin_cls: Plugin, plugin_callback_info: typi
@setup_gauge(BK_PLUGIN_EXECUTE_RUNNING_PROCESSES)
@setup_histogram(BK_PLUGIN_EXECUTE_TIME)
def execute(
self, plugin_cls: Plugin, inputs: typing.Dict[str, typing.Any], context_inputs: typing.Dict[str, typing.Any]
self,
plugin_cls: Plugin,
inputs: typing.Dict[str, typing.Any],
context_inputs: typing.Dict[str, typing.Any],
credentials: typing.Optional[typing.Dict[str, typing.Any]] = None,
) -> ExecuteResult:

# user inputs validation
Expand All @@ -109,7 +113,12 @@ def execute(

# domain object initialization
context = Context(
trace_id=self.trace_id, data=valid_context_inputs, state=State.EMPTY, invoke_count=1, outputs={}
trace_id=self.trace_id,
data=valid_context_inputs,
state=State.EMPTY,
invoke_count=1,
outputs={},
credentials=credentials or {},
)
plugin = plugin_cls()
# run execute method
Expand Down Expand Up @@ -141,8 +150,10 @@ def execute(

# prepare persistent data for schedule
try:
schedule_context = context.schedule_context.copy()
schedule_context["credentials"] = context.credentials
schedule_data = self._dump_schedule_data(
inputs=inputs, context=context.schedule_context, outputs=context.outputs
inputs=inputs, context=schedule_context, outputs=context.outputs
)
except Exception as e:
logger.exception("[execute] schedule data json dumps error")
Expand Down Expand Up @@ -235,6 +246,7 @@ def schedule(self, plugin_cls: Plugin, schedule: Schedule, callback_info: dict =
),
outputs=schedule_data["context"]["outputs"],
storage=schedule_data["context"]["storage"],
credentials=schedule_data["context"].get("credentials", {}),
)
plugin = plugin_cls()
err = ""
Expand All @@ -256,8 +268,10 @@ def schedule(self, plugin_cls: Plugin, schedule: Schedule, callback_info: dict =

context.data = context_inputs_cls(**schedule_data["context"]["data"])
try:
schedule_context = context.schedule_context.copy()
schedule_context["credentials"] = context.credentials
schedule_data = self._dump_schedule_data(
inputs=schedule_data["inputs"], context=context.schedule_context, outputs=context.outputs
inputs=schedule_data["inputs"], context=schedule_context, outputs=context.outputs
)
except Exception:
logger.exception("[execute] schedule data json dumps error")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,62 @@
from bk_plugin_framework.runtime.executor import BKPluginExecutor
from bk_plugin_framework.services.bpf_service.api.permissions import ScopeAllowPermission
from bk_plugin_framework.services.bpf_service.api.serializers import StandardResponseSerializer
from bk_plugin_framework.kit.plugin import Credential

logger = logging.getLogger("bk_plugin")


class InvokeParamsSerializer(serializers.Serializer):
inputs = serializers.DictField(help_text="插件调用参数", required=True)
context = serializers.DictField(help_text="插件执行上下文", required=True)
credentials = serializers.DictField(help_text="插件凭证", required=False, allow_null=True)

def validate(self, attrs):
"""验证凭证是否提供"""
plugin_cls = self.context.get("plugin_cls")
if not plugin_cls:
return attrs

# Check if plugin requires credentials (class attribute, similar to ContextInputs)
credentials_list = []
credentials_cls = getattr(plugin_cls, "Credentials", None)
if credentials_cls:
# Extract all Credential instances from Credentials class
for attr_name in dir(credentials_cls):
if attr_name.startswith("_"):
continue
attr_value = getattr(credentials_cls, attr_name)
if isinstance(attr_value, Credential):
credentials_list.append(attr_value)

if credentials_list:
# Verify that credentials is provided at top level
credentials = attrs.get("credentials")

# Check if credentials is a dict
if not isinstance(credentials, dict):
credential_names = [c.name or c.key for c in credentials_list]
raise serializers.ValidationError(
f"该插件需要凭证({', '.join(credential_names)}),请在请求中提供 credentials 字典"
)

# Check each required credential
missing_credentials = []
for cred_def in credentials_list:
# Check if key exists and value is not None or empty string
if (
cred_def.key not in credentials
or credentials.get(cred_def.key) is None
or credentials.get(cred_def.key) == ""
):
missing_credentials.append(cred_def.name or cred_def.key)

if missing_credentials:
raise serializers.ValidationError(
f"该插件需要以下凭证:{', '.join(missing_credentials)},请在 credentials 中提供这些字段"
)

return attrs


class InvokeResponseSerializer(StandardResponseSerializer):
Expand Down Expand Up @@ -67,21 +116,27 @@ def post(self, request, version):
if not plugin_cls:
return Response(status=status.HTTP_404_NOT_FOUND)

data_serializer = InvokeParamsSerializer(data=request.data)
data_serializer = InvokeParamsSerializer(data=request.data, context={"plugin_cls": plugin_cls})
try:
data_serializer.is_valid(raise_exception=True)
except ValidationError as e:
return Response(
data={"result": False, "data": None, "message": "输入不合法: %s" % e},
data={"result": False, "data": None, "message": "输入不合法: %s" % e, "trace_id": request.trace_id},
status=status.HTTP_400_BAD_REQUEST,
)
request_data = data_serializer.validated_data

# Extract credentials from request data
credentials = request_data.get("credentials") or {}

executor = BKPluginExecutor(trace_id=request.trace_id)

try:
execute_result = executor.execute(
plugin_cls=plugin_cls, inputs=request_data["inputs"], context_inputs=request_data["context"]
plugin_cls=plugin_cls,
inputs=request_data["inputs"],
context_inputs=request_data["context"],
credentials=credentials,
)
except Exception as e:
logging.exception("executor execute raise error")
Expand Down
8 changes: 4 additions & 4 deletions bk-plugin-framework/poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions bk-plugin-framework/pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "bk-plugin-framework"
version = "2.3.2"
version = "2.4.0rc0"
description = "bk plugin python framework"
authors = ["Your Name <you@example.com>"]
license = "MIT"
Expand All @@ -10,7 +10,7 @@ python = "^3.8.0,<4.0"
pydantic = ">=1.0,<3"
werkzeug = ">=2.0.0, <4.0"
apigw-manager = {version = ">=1.0.6, <4", extras = ["extra"]}
bk-plugin-runtime = "2.1.1"
bk-plugin-runtime = "2.2.0rc0"
jsonschema = ">=2.5.0,<5.0.0"

[tool.poetry.dev-dependencies]
Expand Down
Loading