Skip to content

Commit 60f47f4

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

10 files changed

Lines changed: 406 additions & 12 deletions

File tree

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: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,25 @@ class ContextRequire(BaseModel):
5050
pass
5151

5252

53+
class CredentialModel:
54+
"""凭证模型基类,用于声明插件需要的凭证"""
55+
pass
56+
57+
58+
class Credential(BaseModel):
59+
"""凭证定义类,用于声明插件需要的凭证"""
60+
61+
key: str
62+
name: str = ""
63+
description: str = ""
64+
65+
def __init__(self, key: str, name: str = "", description: str = "", **kwargs):
66+
# 如果 name 为空,使用 key 作为 name
67+
if not name:
68+
name = key
69+
super().__init__(key=key, name=name, description=description, **kwargs)
70+
71+
5372
class Callback(object):
5473
def __init__(self, callback_id: str = "", callback_data: dict = {}):
5574
self.id = callback_id
@@ -66,6 +85,7 @@ def __init__(
6685
callback: Callback = None,
6786
outputs: typing.Optional[dict] = None,
6887
storage: typing.Optional[dict] = None,
88+
credentials: typing.Optional[dict] = None,
6989
):
7090
self.trace_id = trace_id
7191
self.data = data
@@ -74,6 +94,7 @@ def __init__(
7494
self.callback = callback
7595
self.storage = storage or {}
7696
self.outputs = outputs or {}
97+
self.credentials = credentials or {}
7798

7899
@property
79100
def schedule_context(self) -> dict:
@@ -140,6 +161,28 @@ def __new__(cls, name, bases, dct):
140161
"plugin deinition error, {}'s ContextInputs is not subclass of {}".format(new_cls, ContextRequire)
141162
)
142163

164+
# credentials validation (class attribute, similar to ContextInputs)
165+
credentials = getattr(new_cls, "credentials", None)
166+
if credentials is None:
167+
credentials = []
168+
if not isinstance(credentials, list):
169+
raise TypeError(
170+
"plugin deinition error, credentials field is not a list in {}".format(new_cls)
171+
)
172+
173+
# Validate each credential in the list
174+
for idx, credential in enumerate(credentials):
175+
if not isinstance(credential, Credential):
176+
raise TypeError(
177+
"plugin deinition error, credentials[{}] is not a Credential instance in {}".format(
178+
idx, new_cls
179+
)
180+
)
181+
if not credential.key:
182+
raise ValueError(
183+
"plugin deinition error, credentials[{}].key cannot be empty in {}".format(idx, new_cls)
184+
)
185+
143186
# inputs form check
144187
inputs_form_cls = getattr(new_cls, "InputsForm", None)
145188
if inputs_form_cls and not issubclass(inputs_form_cls, FormModel):
@@ -200,6 +243,7 @@ def dict(cls) -> dict:
200243
"desc": getattr(cls.Meta, "desc", ""),
201244
"version": cls.Meta.version,
202245
"enable_plugin_callback": getattr(cls.Meta, "enable_plugin_callback", False),
246+
"credentials": cls._EMPTY_SCHEMA,
203247
"inputs": cls._EMPTY_SCHEMA,
204248
"outputs": cls._EMPTY_SCHEMA,
205249
"context_inputs": cls._EMPTY_SCHEMA,
@@ -227,4 +271,16 @@ def dict(cls) -> dict:
227271
if context_cls:
228272
data["context_inputs"] = cls._trim_schema(context_cls.schema())
229273

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

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: 53 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,13 +28,57 @@
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+
if cred_def.key not in credentials or not credentials.get(cred_def.key):
74+
missing_credentials.append(cred_def.name or cred_def.key)
75+
76+
if missing_credentials:
77+
raise serializers.ValidationError(
78+
f"该插件需要以下凭证:{', '.join(missing_credentials)},请在 credentials 中提供这些字段"
79+
)
80+
81+
return attrs
3882

3983

4084
class InvokeResponseSerializer(StandardResponseSerializer):
@@ -67,21 +111,27 @@ def post(self, request, version):
67111
if not plugin_cls:
68112
return Response(status=status.HTTP_404_NOT_FOUND)
69113

70-
data_serializer = InvokeParamsSerializer(data=request.data)
114+
data_serializer = InvokeParamsSerializer(data=request.data, context={"plugin_cls": plugin_cls})
71115
try:
72116
data_serializer.is_valid(raise_exception=True)
73117
except ValidationError as e:
74118
return Response(
75-
data={"result": False, "data": None, "message": "输入不合法: %s" % e},
119+
data={"result": False, "data": None, "message": "输入不合法: %s" % e, "trace_id": request.trace_id},
76120
status=status.HTTP_400_BAD_REQUEST,
77121
)
78122
request_data = data_serializer.validated_data
79123

124+
# Extract credentials from request data
125+
credentials = request_data.get("credentials") or {}
126+
80127
executor = BKPluginExecutor(trace_id=request.trace_id)
81128

82129
try:
83130
execute_result = executor.execute(
84-
plugin_cls=plugin_cls, inputs=request_data["inputs"], context_inputs=request_data["context"]
131+
plugin_cls=plugin_cls,
132+
inputs=request_data["inputs"],
133+
context_inputs=request_data["context"],
134+
credentials=credentials,
85135
)
86136
except Exception as e:
87137
logging.exception("executor execute raise error")

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 <you@example.com>"]
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)