Skip to content

Commit c911cf5

Browse files
committed
feat: 插件支持声明依赖用户登录态 --story=125449007
1 parent bcbe891 commit c911cf5

File tree

38 files changed

+1995
-1102
lines changed

38 files changed

+1995
-1102
lines changed

bk-plugin-framework/bk_plugin_framework/__version__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,4 @@
1010
specific language governing permissions and limitations under the License.
1111
"""
1212

13-
__version__ = "2.3.2"
13+
__version__ = "2.4.0rc0"

bk-plugin-framework/bk_plugin_framework/kit/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,4 +23,6 @@
2323
State,
2424
Callback,
2525
FormModel,
26+
Credential,
27+
CredentialModel,
2628
)

bk-plugin-framework/bk_plugin_framework/kit/plugin.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,26 @@ class ContextRequire(BaseModel):
5050
pass
5151

5252

53+
class CredentialModel:
54+
"""凭证模型基类,用于声明插件需要的凭证"""
55+
56+
pass
57+
58+
59+
class Credential(BaseModel):
60+
"""凭证定义类,用于声明插件需要的凭证"""
61+
62+
key: str
63+
name: str = ""
64+
description: str = ""
65+
66+
def __init__(self, key: str, name: str = "", description: str = "", **kwargs):
67+
# 如果 name 为空,使用 key 作为 name
68+
if not name:
69+
name = key
70+
super().__init__(key=key, name=name, description=description, **kwargs)
71+
72+
5373
class Callback(object):
5474
def __init__(self, callback_id: str = "", callback_data: dict = {}):
5575
self.id = callback_id
@@ -66,6 +86,7 @@ def __init__(
6686
callback: Callback = None,
6787
outputs: typing.Optional[dict] = None,
6888
storage: typing.Optional[dict] = None,
89+
credentials: typing.Optional[dict] = None,
6990
):
7091
self.trace_id = trace_id
7192
self.data = data
@@ -74,6 +95,7 @@ def __init__(
7495
self.callback = callback
7596
self.storage = storage or {}
7697
self.outputs = outputs or {}
98+
self.credentials = credentials or {}
7799

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

165+
# credentials validation (class attribute, similar to ContextInputs)
166+
credentials_cls = getattr(new_cls, "Credentials", None)
167+
if credentials_cls:
168+
# Check if Credentials class inherits from CredentialModel
169+
if not issubclass(credentials_cls, CredentialModel):
170+
raise TypeError(
171+
"plugin deinition error, {}'s Credentials is not subclass of {}".format(new_cls, CredentialModel)
172+
)
173+
174+
# Validate each Credential instance in the Credentials class
175+
for attr_name in dir(credentials_cls):
176+
if attr_name.startswith("_"):
177+
continue
178+
attr_value = getattr(credentials_cls, attr_name)
179+
if isinstance(attr_value, Credential):
180+
if not attr_value.key:
181+
raise ValueError(
182+
"plugin deinition error, Credentials.{}.key cannot be empty in {}".format(
183+
attr_name, new_cls
184+
)
185+
)
186+
143187
# inputs form check
144188
inputs_form_cls = getattr(new_cls, "InputsForm", None)
145189
if inputs_form_cls and not issubclass(inputs_form_cls, FormModel):
@@ -200,6 +244,7 @@ def dict(cls) -> dict:
200244
"desc": getattr(cls.Meta, "desc", ""),
201245
"version": cls.Meta.version,
202246
"enable_plugin_callback": getattr(cls.Meta, "enable_plugin_callback", False),
247+
"credentials": cls._EMPTY_SCHEMA,
203248
"inputs": cls._EMPTY_SCHEMA,
204249
"outputs": cls._EMPTY_SCHEMA,
205250
"context_inputs": cls._EMPTY_SCHEMA,
@@ -227,4 +272,18 @@ def dict(cls) -> dict:
227272
if context_cls:
228273
data["context_inputs"] = cls._trim_schema(context_cls.schema())
229274

275+
# Extract credentials from Credentials class
276+
credentials_cls = getattr(cls, "Credentials", None)
277+
if credentials_cls:
278+
credentials_list = []
279+
for attr_name in dir(credentials_cls):
280+
if attr_name.startswith("_"):
281+
continue
282+
attr_value = getattr(credentials_cls, attr_name)
283+
if isinstance(attr_value, Credential):
284+
credentials_list.append(attr_value)
285+
286+
data["credentials"] = [
287+
{"key": c.key, "name": c.name, "description": c.description} for c in credentials_list
288+
]
230289
return data

bk-plugin-framework/bk_plugin_framework/metrics.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -129,14 +129,16 @@ def _wrapper(*args, **kwargs):
129129

130130
# 正在执行的 EXECUTE 数量
131131
BK_PLUGIN_EXECUTE_RUNNING_PROCESSES = Gauge(
132-
name="bk_plugin_execute_running_processes", documentation="count running state processes",
133-
labelnames=["hostname", "version"]
132+
name="bk_plugin_execute_running_processes",
133+
documentation="count running state processes",
134+
labelnames=["hostname", "version"],
134135
)
135136

136137
# 正在执行的 SCHEDULE 数量
137138
BK_PLUGIN_SCHEDULE_RUNNING_PROCESSES = Gauge(
138-
name="bk_plugin_schedule_running_processes", documentation="count running state schedules",
139-
labelnames=["hostname", "version"]
139+
name="bk_plugin_schedule_running_processes",
140+
documentation="count running state schedules",
141+
labelnames=["hostname", "version"],
140142
)
141143

142144
# CALLBACK 异常次数

bk-plugin-framework/bk_plugin_framework/runtime/callback/celery/tasks.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,4 +80,5 @@ def callback(trace_id: str, callback_id: str, callback_data: str):
8080
_set_schedule_state(trace_id=trace_id, state=State.FAIL)
8181

8282
BK_PLUGIN_CALLBACK_TIME.labels(hostname=HOSTNAME, version=schedule.plugin_version).observe(
83-
time.perf_counter() - start)
83+
time.perf_counter() - start
84+
)

bk-plugin-framework/bk_plugin_framework/runtime/executor.py

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,11 @@ def _plugin_finish_callback(self, plugin_cls: Plugin, plugin_callback_info: typi
9090
@setup_gauge(BK_PLUGIN_EXECUTE_RUNNING_PROCESSES)
9191
@setup_histogram(BK_PLUGIN_EXECUTE_TIME)
9292
def execute(
93-
self, plugin_cls: Plugin, inputs: typing.Dict[str, typing.Any], context_inputs: typing.Dict[str, typing.Any]
93+
self,
94+
plugin_cls: Plugin,
95+
inputs: typing.Dict[str, typing.Any],
96+
context_inputs: typing.Dict[str, typing.Any],
97+
credentials: typing.Optional[typing.Dict[str, typing.Any]] = None,
9498
) -> ExecuteResult:
9599

96100
# user inputs validation
@@ -109,7 +113,12 @@ def execute(
109113

110114
# domain object initialization
111115
context = Context(
112-
trace_id=self.trace_id, data=valid_context_inputs, state=State.EMPTY, invoke_count=1, outputs={}
116+
trace_id=self.trace_id,
117+
data=valid_context_inputs,
118+
state=State.EMPTY,
119+
invoke_count=1,
120+
outputs={},
121+
credentials=credentials or {},
113122
)
114123
plugin = plugin_cls()
115124
# run execute method
@@ -141,8 +150,10 @@ def execute(
141150

142151
# prepare persistent data for schedule
143152
try:
153+
schedule_context = context.schedule_context.copy()
154+
schedule_context["credentials"] = context.credentials
144155
schedule_data = self._dump_schedule_data(
145-
inputs=inputs, context=context.schedule_context, outputs=context.outputs
156+
inputs=inputs, context=schedule_context, outputs=context.outputs
146157
)
147158
except Exception as e:
148159
logger.exception("[execute] schedule data json dumps error")
@@ -235,6 +246,7 @@ def schedule(self, plugin_cls: Plugin, schedule: Schedule, callback_info: dict =
235246
),
236247
outputs=schedule_data["context"]["outputs"],
237248
storage=schedule_data["context"]["storage"],
249+
credentials=schedule_data["context"].get("credentials", {}),
238250
)
239251
plugin = plugin_cls()
240252
err = ""
@@ -256,8 +268,10 @@ def schedule(self, plugin_cls: Plugin, schedule: Schedule, callback_info: dict =
256268

257269
context.data = context_inputs_cls(**schedule_data["context"]["data"])
258270
try:
271+
schedule_context = context.schedule_context.copy()
272+
schedule_context["credentials"] = context.credentials
259273
schedule_data = self._dump_schedule_data(
260-
inputs=schedule_data["inputs"], context=context.schedule_context, outputs=context.outputs
274+
inputs=schedule_data["inputs"], context=schedule_context, outputs=context.outputs
261275
)
262276
except Exception:
263277
logger.exception("[execute] schedule data json dumps error")

bk-plugin-framework/bk_plugin_framework/services/bpf_service/api/invoke.py

Lines changed: 58 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,13 +28,62 @@
2828
from bk_plugin_framework.runtime.executor import BKPluginExecutor
2929
from bk_plugin_framework.services.bpf_service.api.permissions import ScopeAllowPermission
3030
from bk_plugin_framework.services.bpf_service.api.serializers import StandardResponseSerializer
31+
from bk_plugin_framework.kit.plugin import Credential
3132

3233
logger = logging.getLogger("bk_plugin")
3334

3435

3536
class InvokeParamsSerializer(serializers.Serializer):
3637
inputs = serializers.DictField(help_text="插件调用参数", required=True)
3738
context = serializers.DictField(help_text="插件执行上下文", required=True)
39+
credentials = serializers.DictField(help_text="插件凭证", required=False, allow_null=True)
40+
41+
def validate(self, attrs):
42+
"""验证凭证是否提供"""
43+
plugin_cls = self.context.get("plugin_cls")
44+
if not plugin_cls:
45+
return attrs
46+
47+
# Check if plugin requires credentials (class attribute, similar to ContextInputs)
48+
credentials_list = []
49+
credentials_cls = getattr(plugin_cls, "Credentials", None)
50+
if credentials_cls:
51+
# Extract all Credential instances from Credentials class
52+
for attr_name in dir(credentials_cls):
53+
if attr_name.startswith("_"):
54+
continue
55+
attr_value = getattr(credentials_cls, attr_name)
56+
if isinstance(attr_value, Credential):
57+
credentials_list.append(attr_value)
58+
59+
if credentials_list:
60+
# Verify that credentials is provided at top level
61+
credentials = attrs.get("credentials")
62+
63+
# Check if credentials is a dict
64+
if not isinstance(credentials, dict):
65+
credential_names = [c.name or c.key for c in credentials_list]
66+
raise serializers.ValidationError(
67+
f"该插件需要凭证({', '.join(credential_names)}),请在请求中提供 credentials 字典"
68+
)
69+
70+
# Check each required credential
71+
missing_credentials = []
72+
for cred_def in credentials_list:
73+
# Check if key exists and value is not None or empty string
74+
if (
75+
cred_def.key not in credentials
76+
or credentials.get(cred_def.key) is None
77+
or credentials.get(cred_def.key) == ""
78+
):
79+
missing_credentials.append(cred_def.name or cred_def.key)
80+
81+
if missing_credentials:
82+
raise serializers.ValidationError(
83+
f"该插件需要以下凭证:{', '.join(missing_credentials)},请在 credentials 中提供这些字段"
84+
)
85+
86+
return attrs
3887

3988

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

70-
data_serializer = InvokeParamsSerializer(data=request.data)
119+
data_serializer = InvokeParamsSerializer(data=request.data, context={"plugin_cls": plugin_cls})
71120
try:
72121
data_serializer.is_valid(raise_exception=True)
73122
except ValidationError as e:
74123
return Response(
75-
data={"result": False, "data": None, "message": "输入不合法: %s" % e},
124+
data={"result": False, "data": None, "message": "输入不合法: %s" % e, "trace_id": request.trace_id},
76125
status=status.HTTP_400_BAD_REQUEST,
77126
)
78127
request_data = data_serializer.validated_data
79128

129+
# Extract credentials from request data
130+
credentials = request_data.get("credentials") or {}
131+
80132
executor = BKPluginExecutor(trace_id=request.trace_id)
81133

82134
try:
83135
execute_result = executor.execute(
84-
plugin_cls=plugin_cls, inputs=request_data["inputs"], context_inputs=request_data["context"]
136+
plugin_cls=plugin_cls,
137+
inputs=request_data["inputs"],
138+
context_inputs=request_data["context"],
139+
credentials=credentials,
85140
)
86141
except Exception as e:
87142
logging.exception("executor execute raise error")

bk-plugin-framework/poetry.lock

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

bk-plugin-framework/pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
22
name = "bk-plugin-framework"
3-
version = "2.3.2"
3+
version = "2.4.0rc0"
44
description = "bk plugin python framework"
55
authors = ["Your Name <[email protected]>"]
66
license = "MIT"
@@ -10,7 +10,7 @@ python = "^3.8.0,<4.0"
1010
pydantic = ">=1.0,<3"
1111
werkzeug = ">=2.0.0, <4.0"
1212
apigw-manager = {version = ">=1.0.6, <4", extras = ["extra"]}
13-
bk-plugin-runtime = "2.1.1"
13+
bk-plugin-runtime = "2.2.0rc0"
1414
jsonschema = ">=2.5.0,<5.0.0"
1515

1616
[tool.poetry.dev-dependencies]

0 commit comments

Comments
 (0)