mirror of
https://github.com/xhongc/music-tag-web.git
synced 2026-05-12 02:55:58 +08:00
feature:dabao
This commit is contained in:
5
.idea/bamboo_engine_playground.iml
generated
5
.idea/bamboo_engine_playground.iml
generated
@@ -26,5 +26,10 @@
|
||||
</component>
|
||||
<component name="TemplatesService">
|
||||
<option name="TEMPLATE_CONFIGURATION" value="Django" />
|
||||
<option name="TEMPLATE_FOLDERS">
|
||||
<list>
|
||||
<option value="$MODULE_DIR$/templates" />
|
||||
</list>
|
||||
</option>
|
||||
</component>
|
||||
</module>
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,24 +0,0 @@
|
||||
# Generated by Django 2.2.6 on 2022-02-25 15:31
|
||||
|
||||
from django.db import migrations
|
||||
import django_mysql.models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('flow', '0003_auto_20220210_1737'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='nodetemplate',
|
||||
name='input_component',
|
||||
field=django_mysql.models.JSONField(default=list, verbose_name='前端输入参数组件'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='nodetemplate',
|
||||
name='output_component',
|
||||
field=django_mysql.models.JSONField(default=list, verbose_name='前端输出参数组件'),
|
||||
),
|
||||
]
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,16 +1,14 @@
|
||||
from pathlib import Path
|
||||
import sys
|
||||
import os
|
||||
|
||||
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
||||
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||
sys.path.insert(1, os.path.join(os.getcwd(), 'lib'))
|
||||
|
||||
BROKER_URL = "redis://localhost:6379/3"
|
||||
# Quick-start development settings - unsuitable for production
|
||||
# See https://docs.djangoproject.com/en/3.2/howto/deployment/checklist/
|
||||
|
||||
# SECURITY WARNING: keep the secret key used in production secret!
|
||||
SECRET_KEY = 'django-insecure-u5_r=pekio0@zt!y(kgbufuosb9mddu8*qeejkzj@=7uyvb392'
|
||||
|
||||
# SECURITY WARNING: don't run with debug turned on in production!
|
||||
DEBUG = True
|
||||
|
||||
ALLOWED_HOSTS = ["*"]
|
||||
@@ -57,7 +55,7 @@ ROOT_URLCONF = 'dj_flow.urls'
|
||||
TEMPLATES = [
|
||||
{
|
||||
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
||||
'DIRS': [],
|
||||
'DIRS': [os.path.join(BASE_DIR, "templates")],
|
||||
'APP_DIRS': True,
|
||||
'OPTIONS': {
|
||||
'context_processors': [
|
||||
@@ -114,6 +112,7 @@ USE_L10N = True
|
||||
USE_TZ = False
|
||||
|
||||
STATIC_URL = '/static/'
|
||||
STATICFILES_DIRS = [os.path.join(BASE_DIR, "static")] # noqa
|
||||
|
||||
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
|
||||
IS_USE_CELERY = True
|
||||
@@ -142,4 +141,4 @@ REST_FRAMEWORK = {
|
||||
|
||||
# 本地调试
|
||||
# CELERY_ALWAYS_EAGER = True
|
||||
# CELERY_TASK_ALWAYS_EAGER = True
|
||||
# CELERY_TASK_ALWAYS_EAGER = True
|
||||
|
||||
@@ -16,9 +16,11 @@ Including another URLconf
|
||||
from django.contrib import admin
|
||||
from django.urls import path, include
|
||||
from applications.flow.urls import flow_router, node_router
|
||||
from dj_flow.views import index
|
||||
|
||||
urlpatterns = [
|
||||
path('admin/', admin.site.urls),
|
||||
path('', index),
|
||||
path("process/", include(flow_router.urls)),
|
||||
path("node/", include(node_router.urls)),
|
||||
|
||||
|
||||
5
dj_flow/views.py
Normal file
5
dj_flow/views.py
Normal file
@@ -0,0 +1,5 @@
|
||||
from django.shortcuts import render
|
||||
|
||||
|
||||
def index(request):
|
||||
return render(request, "index.html")
|
||||
14
lib/bamboo_engine/__init__.py
Normal file
14
lib/bamboo_engine/__init__.py
Normal file
@@ -0,0 +1,14 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS Community
|
||||
Edition) available.
|
||||
Copyright (C) 2017-2021 THL A29 Limited, a Tencent company. All rights reserved.
|
||||
Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
http://opensource.org/licenses/MIT
|
||||
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.
|
||||
"""
|
||||
|
||||
from .engine import Engine # noqa
|
||||
14
lib/bamboo_engine/__version__.py
Normal file
14
lib/bamboo_engine/__version__.py
Normal file
@@ -0,0 +1,14 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS Community
|
||||
Edition) available.
|
||||
Copyright (C) 2017-2021 THL A29 Limited, a Tencent company. All rights reserved.
|
||||
Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
http://opensource.org/licenses/MIT
|
||||
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.
|
||||
"""
|
||||
|
||||
__version__ = "1.6.4"
|
||||
647
lib/bamboo_engine/api.py
Normal file
647
lib/bamboo_engine/api.py
Normal file
@@ -0,0 +1,647 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS Community
|
||||
Edition) available.
|
||||
Copyright (C) 2017-2021 THL A29 Limited, a Tencent company. All rights reserved.
|
||||
Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
http://opensource.org/licenses/MIT
|
||||
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.
|
||||
"""
|
||||
|
||||
|
||||
# API 模块用于向外暴露接口,bamboo-engine 的使用者应该永远只用这个模块与 bamboo-engien 进行交互
|
||||
|
||||
|
||||
import logging
|
||||
import functools
|
||||
import traceback
|
||||
from typing import Optional, Any, List
|
||||
|
||||
from .utils.object import Representable
|
||||
from .eri import EngineRuntimeInterface, ContextValue
|
||||
from .engine import Engine
|
||||
from .template import Template
|
||||
from .context import Context
|
||||
from .utils.constants import VAR_CONTEXT_MAPPING
|
||||
|
||||
logger = logging.getLogger("bamboo_engine")
|
||||
|
||||
|
||||
class EngineAPIResult(Representable):
|
||||
"""
|
||||
api 统一返回结果
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
result: bool,
|
||||
message: str,
|
||||
exc: Optional[Exception] = None,
|
||||
data: Optional[Any] = None,
|
||||
exc_trace: Optional[str] = None,
|
||||
):
|
||||
"""
|
||||
:param result: 是否执行成功
|
||||
:type result: bool
|
||||
:param message: 附加消息,result 为 False 时关注
|
||||
:type message: str
|
||||
:param exc: 异常对象
|
||||
:type exc: Exception
|
||||
:param data: 数据
|
||||
:type data: Any
|
||||
"""
|
||||
self.result = result
|
||||
self.message = message
|
||||
self.exc = exc
|
||||
self.data = data
|
||||
self.exc_trace = exc_trace
|
||||
|
||||
|
||||
def _ensure_return_api_result(func):
|
||||
@functools.wraps(func)
|
||||
def wrapper(*args, **kwargs):
|
||||
try:
|
||||
data = func(*args, **kwargs)
|
||||
except Exception as e:
|
||||
logger.exception("{} raise error.".format(func.__name__))
|
||||
trace = traceback.format_exc()
|
||||
return EngineAPIResult(result=False, message="fail", exc=e, data=None, exc_trace=trace)
|
||||
|
||||
if isinstance(data, EngineAPIResult):
|
||||
return data
|
||||
return EngineAPIResult(result=True, message="success", exc=None, data=data, exc_trace=None)
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
@_ensure_return_api_result
|
||||
def run_pipeline(runtime: EngineRuntimeInterface, pipeline: dict, **options) -> EngineAPIResult:
|
||||
"""
|
||||
执行 pipeline
|
||||
|
||||
:param runtime: 引擎运行时实例
|
||||
:type runtime: EngineRuntimeInterface
|
||||
:param pipeline: pipeline 描述对象
|
||||
:type pipeline: dict
|
||||
:return: 执行结果
|
||||
:rtype: EngineAPIResult
|
||||
"""
|
||||
|
||||
Engine(runtime).run_pipeline(pipeline, **options)
|
||||
|
||||
|
||||
@_ensure_return_api_result
|
||||
def pause_pipeline(runtime: EngineRuntimeInterface, pipeline_id: str) -> EngineAPIResult:
|
||||
"""
|
||||
暂停 pipeline 的执行
|
||||
|
||||
:param runtime: 引擎运行时实例
|
||||
:type runtime: EngineRuntimeInterface
|
||||
:param pipeline_id: piipeline id
|
||||
:type pipeline_id: str
|
||||
:return: 执行结果
|
||||
:rtype: EngineAPIResult
|
||||
"""
|
||||
|
||||
Engine(runtime).pause_pipeline(pipeline_id)
|
||||
|
||||
|
||||
@_ensure_return_api_result
|
||||
def revoke_pipeline(runtime: EngineRuntimeInterface, pipeline_id: str) -> EngineAPIResult:
|
||||
"""
|
||||
撤销 pipeline,使其无法继续执行
|
||||
|
||||
:param runtime: 引擎运行时实例
|
||||
:type runtime: EngineRuntimeInterface
|
||||
:param pipeline_id: pipeline id
|
||||
:type pipeline_id: str
|
||||
:return: 执行结果
|
||||
:rtype: EngineAPIResult
|
||||
"""
|
||||
Engine(runtime).revoke_pipeline(pipeline_id)
|
||||
|
||||
|
||||
@_ensure_return_api_result
|
||||
def resume_pipeline(runtime: EngineRuntimeInterface, pipeline_id: str) -> EngineAPIResult:
|
||||
"""
|
||||
继续被 pause_pipeline 接口暂停的 pipeline 的执行
|
||||
|
||||
:param runtime: 引擎运行时实例
|
||||
:type runtime: EngineRuntimeInterface
|
||||
:param pipeline_id: pipeline id
|
||||
:type pipeline_id: str
|
||||
:return: 执行结果
|
||||
:rtype: EngineAPIResult
|
||||
"""
|
||||
Engine(runtime).resume_pipeline(pipeline_id)
|
||||
|
||||
|
||||
@_ensure_return_api_result
|
||||
def pause_node_appoint(runtime: EngineRuntimeInterface, node_id: str) -> EngineAPIResult:
|
||||
"""
|
||||
预约暂停某个节点的执行
|
||||
|
||||
:param runtime: 引擎运行时实例
|
||||
:type runtime: EngineRuntimeInterface
|
||||
:param node_id: 节点 id
|
||||
:type node_id: str
|
||||
:return: 执行结果
|
||||
:rtype: EngineAPIResult
|
||||
"""
|
||||
Engine(runtime).pause_node_appoint(node_id)
|
||||
|
||||
|
||||
@_ensure_return_api_result
|
||||
def resume_node_appoint(runtime: EngineRuntimeInterface, node_id: str) -> EngineAPIResult:
|
||||
"""
|
||||
继续由于某个节点而暂停的 pipeline 的执行
|
||||
|
||||
:param runtime: 引擎运行时实例
|
||||
:type runtime: EngineRuntimeInterface
|
||||
:param node_id: 节点 id
|
||||
:type node_id: str
|
||||
:return: 执行结果
|
||||
:rtype: EngineAPIResult
|
||||
"""
|
||||
Engine(runtime).resume_node_appoint(node_id)
|
||||
|
||||
|
||||
@_ensure_return_api_result
|
||||
def retry_node(runtime: EngineRuntimeInterface, node_id: str, data: Optional[dict] = None) -> EngineAPIResult:
|
||||
"""
|
||||
重试某个执行失败的节点
|
||||
|
||||
:param runtime: 引擎运行时实例
|
||||
:type runtime: EngineRuntimeInterface
|
||||
:param node_id: 失败的节点 id
|
||||
:type node_id: str
|
||||
:param data: 重试时使用的节点执行输入
|
||||
:type data: dict
|
||||
:return: 执行结果
|
||||
:rtype: EngineAPIResult
|
||||
"""
|
||||
Engine(runtime).retry_node(node_id, data)
|
||||
|
||||
|
||||
@_ensure_return_api_result
|
||||
def retry_subprocess(runtime: EngineRuntimeInterface, node_id: str) -> EngineAPIResult:
|
||||
"""
|
||||
重试进入失败的子流程节点
|
||||
|
||||
:param runtime: 引擎运行时实例
|
||||
:type runtime: EngineRuntimeInterface
|
||||
:param node_id: 子流程节点 id
|
||||
:type node_id: str
|
||||
:return: [description]
|
||||
:rtype: EngineAPIResult
|
||||
"""
|
||||
Engine(runtime).retry_subprocess(node_id)
|
||||
|
||||
|
||||
@_ensure_return_api_result
|
||||
def skip_node(runtime: EngineRuntimeInterface, node_id: str) -> EngineAPIResult:
|
||||
"""
|
||||
跳过某个执行失败的节点(仅限 event,activity)
|
||||
|
||||
:param runtime: 引擎运行时实例
|
||||
:type runtime: EngineRuntimeInterface
|
||||
:param node_id: 失败的节点 id
|
||||
:type node_id: str
|
||||
:return: 执行结果
|
||||
:rtype: EngineAPIResult
|
||||
"""
|
||||
Engine(runtime).skip_node(node_id)
|
||||
|
||||
|
||||
@_ensure_return_api_result
|
||||
def skip_exclusive_gateway(runtime: EngineRuntimeInterface, node_id: str, flow_id: str) -> EngineAPIResult:
|
||||
"""
|
||||
跳过某个执行失败的分支网关
|
||||
|
||||
:param runtime: 引擎运行时实例
|
||||
:type runtime: EngineRuntimeInterface
|
||||
:param node_id: 失败的分支网关 id
|
||||
:type node_id: str
|
||||
:param flow_id: 需要往下执行的 flow id
|
||||
:type flow_id: str
|
||||
:return: 执行结果
|
||||
:rtype: EngineAPIResult
|
||||
"""
|
||||
Engine(runtime).skip_exclusive_gateway(node_id, flow_id)
|
||||
|
||||
|
||||
@_ensure_return_api_result
|
||||
def skip_conditional_parallel_gateway(
|
||||
runtime: EngineRuntimeInterface,
|
||||
node_id: str,
|
||||
flow_ids: list,
|
||||
converge_gateway_id: str,
|
||||
) -> EngineAPIResult:
|
||||
"""
|
||||
跳过某个执行失败的条件并行网关
|
||||
|
||||
:param runtime: 引擎运行时实例
|
||||
:type runtime: EngineRuntimeInterface
|
||||
:param node_id: 失败的分支网关 id
|
||||
:type node_id: str
|
||||
:param flow_ids: 需要往下执行的 flow id 列表
|
||||
:type flow_ids: list
|
||||
:param converge_gateway_id: 目标汇聚网关 id
|
||||
:type converge_gateway_id: str
|
||||
:return: 执行结果
|
||||
:rtype: EngineAPIResult
|
||||
"""
|
||||
Engine(runtime).skip_conditional_parallel_gateway(node_id, flow_ids, converge_gateway_id)
|
||||
|
||||
|
||||
@_ensure_return_api_result
|
||||
def forced_fail_activity(runtime: EngineRuntimeInterface, node_id: str, ex_data: str) -> EngineAPIResult:
|
||||
"""
|
||||
强制失败某个 activity 节点
|
||||
|
||||
:param runtime: 引擎运行时实例
|
||||
:type runtime: EngineRuntimeInterface
|
||||
:param node_id: 节点 ID
|
||||
:type node_id: str
|
||||
:param message: 异常信息
|
||||
:type message: str
|
||||
:return: 执行结果
|
||||
:rtype: EngineAPIResult
|
||||
"""
|
||||
Engine(runtime).forced_fail_activity(node_id, ex_data)
|
||||
|
||||
|
||||
@_ensure_return_api_result
|
||||
def callback(runtime: EngineRuntimeInterface, node_id: str, version: str, data: dict) -> EngineAPIResult:
|
||||
"""
|
||||
回调某个节点
|
||||
|
||||
:param runtime: 引擎运行时实例
|
||||
:type runtime: EngineRuntimeInterface
|
||||
:param version: 节点执行版本
|
||||
:param version: str
|
||||
:param data: 节点 ID
|
||||
:type data: dict
|
||||
:return: 执行结果
|
||||
:rtype: EngineAPIResult
|
||||
"""
|
||||
Engine(runtime).callback(node_id, version, data)
|
||||
|
||||
|
||||
@_ensure_return_api_result
|
||||
def get_pipeline_states(runtime: EngineRuntimeInterface, root_id: str, flat_children=True) -> EngineAPIResult:
|
||||
"""
|
||||
返回某个任务的状态树
|
||||
|
||||
:param runtime: 引擎运行时实例
|
||||
:type runtime: EngineRuntimeInterface
|
||||
:param root_id: 根节点 ID
|
||||
:type root_id: str
|
||||
:param flat_children: 是否将所有子节点展开
|
||||
:type flat_children: bool
|
||||
:return: 执行结果
|
||||
:rtype: EngineAPIResult
|
||||
"""
|
||||
states = runtime.get_state_by_root(root_id)
|
||||
if not states:
|
||||
return {}
|
||||
|
||||
root_state = None
|
||||
children = {}
|
||||
for s in states:
|
||||
if s.node_id != root_id:
|
||||
children[s.node_id] = {
|
||||
"id": s.node_id,
|
||||
"state": s.name,
|
||||
"root_id:": s.root_id,
|
||||
"parent_id": s.parent_id,
|
||||
"version": s.version,
|
||||
"loop": s.loop,
|
||||
"retry": s.retry,
|
||||
"skip": s.skip,
|
||||
"error_ignorable": s.error_ignored,
|
||||
"error_ignored": s.error_ignored,
|
||||
"created_time": s.created_time,
|
||||
"started_time": s.started_time,
|
||||
"archived_time": s.archived_time,
|
||||
"children": {},
|
||||
}
|
||||
else:
|
||||
root_state = s
|
||||
|
||||
if not flat_children:
|
||||
# set node children
|
||||
for node_id, state in children.items():
|
||||
if state["parent_id"] in children:
|
||||
children[state["parent_id"]]["children"][node_id] = state
|
||||
|
||||
# pop sub child
|
||||
for node_id in list(children.keys()):
|
||||
if children[node_id]["parent_id"] != root_state.node_id:
|
||||
children.pop(node_id)
|
||||
|
||||
state_tree = {}
|
||||
state_tree[root_state.node_id] = {
|
||||
"id": root_state.node_id,
|
||||
"state": root_state.name,
|
||||
"root_id:": root_state.root_id,
|
||||
"parent_id": root_state.root_id,
|
||||
"version": root_state.version,
|
||||
"loop": root_state.loop,
|
||||
"retry": root_state.retry,
|
||||
"skip": root_state.skip,
|
||||
"error_ignorable": s.error_ignored,
|
||||
"error_ignored": s.error_ignored,
|
||||
"created_time": root_state.created_time,
|
||||
"started_time": root_state.started_time,
|
||||
"archived_time": root_state.archived_time,
|
||||
"children": children,
|
||||
}
|
||||
return state_tree
|
||||
|
||||
|
||||
@_ensure_return_api_result
|
||||
def get_children_states(runtime: EngineRuntimeInterface, node_id: str) -> EngineAPIResult:
|
||||
"""
|
||||
返回某个节点及其所有子节点的状态
|
||||
|
||||
:param runtime: 引擎运行时实例
|
||||
:type runtime: EngineRuntimeInterface
|
||||
:param node_id: 父流程 ID
|
||||
:type node_id: str
|
||||
:return: 执行结果
|
||||
:rtype: EngineAPIResult
|
||||
"""
|
||||
parent_state = runtime.get_state_or_none(node_id)
|
||||
if not parent_state:
|
||||
return {}
|
||||
|
||||
states = runtime.get_state_by_parent(node_id)
|
||||
children = {}
|
||||
for s in states:
|
||||
children[s.node_id] = {
|
||||
"id": s.node_id,
|
||||
"state": s.name,
|
||||
"root_id:": s.root_id,
|
||||
"parent_id": s.parent_id,
|
||||
"version": s.version,
|
||||
"loop": s.loop,
|
||||
"retry": s.retry,
|
||||
"skip": s.skip,
|
||||
"error_ignorable": s.error_ignored,
|
||||
"error_ignored": s.error_ignored,
|
||||
"created_time": s.created_time,
|
||||
"started_time": s.started_time,
|
||||
"archived_time": s.archived_time,
|
||||
"children": {},
|
||||
}
|
||||
|
||||
state_tree = {}
|
||||
state_tree[parent_state.node_id] = {
|
||||
"id": parent_state.node_id,
|
||||
"state": parent_state.name,
|
||||
"root_id:": parent_state.root_id,
|
||||
"parent_id": parent_state.root_id,
|
||||
"version": parent_state.version,
|
||||
"loop": parent_state.loop,
|
||||
"retry": parent_state.retry,
|
||||
"skip": parent_state.skip,
|
||||
"error_ignorable": parent_state.error_ignored,
|
||||
"error_ignored": parent_state.error_ignored,
|
||||
"created_time": parent_state.created_time,
|
||||
"started_time": parent_state.started_time,
|
||||
"archived_time": parent_state.archived_time,
|
||||
"children": children,
|
||||
}
|
||||
return state_tree
|
||||
|
||||
|
||||
@_ensure_return_api_result
|
||||
def get_execution_data_inputs(runtime: EngineRuntimeInterface, node_id: str) -> EngineAPIResult:
|
||||
"""
|
||||
获取某个节点执行数据的输入数据
|
||||
|
||||
:param runtime: 引擎运行时实例
|
||||
:type runtime: EngineRuntimeInterface
|
||||
:param node_id: 节点 ID
|
||||
:type node_id: str
|
||||
:return: 执行结果
|
||||
:rtype: EngineAPIResult
|
||||
"""
|
||||
return runtime.get_execution_data_inputs(node_id)
|
||||
|
||||
|
||||
@_ensure_return_api_result
|
||||
def get_execution_data_outputs(runtime: EngineRuntimeInterface, node_id: str) -> EngineAPIResult:
|
||||
"""
|
||||
获取某个节点的执行数据输出
|
||||
|
||||
:param runtime: 引擎运行时实例
|
||||
:type runtime: EngineRuntimeInterface
|
||||
:param node_id: 节点 ID
|
||||
:type node_id: str
|
||||
:return: 执行结果
|
||||
:rtype: EngineAPIResult
|
||||
"""
|
||||
return runtime.get_execution_data_outputs(node_id)
|
||||
|
||||
|
||||
@_ensure_return_api_result
|
||||
def get_execution_data(runtime: EngineRuntimeInterface, node_id: str) -> EngineAPIResult:
|
||||
"""
|
||||
获取某个节点的执行数据
|
||||
|
||||
:param runtime: 引擎运行时实例
|
||||
:type runtime: EngineRuntimeInterface
|
||||
:param node_id: 节点 ID
|
||||
:type node_id: str
|
||||
:return: 执行结果
|
||||
:rtype: EngineAPIResult
|
||||
"""
|
||||
data = runtime.get_execution_data(node_id)
|
||||
return {"inputs": data.inputs, "outputs": data.outputs}
|
||||
|
||||
|
||||
@_ensure_return_api_result
|
||||
def get_data(runtime: EngineRuntimeInterface, node_id: str) -> EngineAPIResult:
|
||||
"""
|
||||
获取某个节点的原始输入数据
|
||||
|
||||
:param runtime: 引擎运行时实例
|
||||
:type runtime: EngineRuntimeInterface
|
||||
:param node_id: 节点 ID
|
||||
:type node_id: str
|
||||
:return: 执行结果
|
||||
:rtype: EngineAPIResult
|
||||
"""
|
||||
data = runtime.get_data(node_id)
|
||||
return {
|
||||
"inputs": {k: {"need_render": v.need_render, "value": v.value} for k, v in data.inputs.items()},
|
||||
"outputs": data.outputs,
|
||||
}
|
||||
|
||||
|
||||
@_ensure_return_api_result
|
||||
def get_node_histories(runtime: EngineRuntimeInterface, node_id: str, loop: int = -1) -> EngineAPIResult:
|
||||
"""
|
||||
获取某个节点的历史记录概览
|
||||
|
||||
:param runtime: 引擎运行时实例
|
||||
:type runtime: EngineRuntimeInterface
|
||||
:param node_id: 节点 ID
|
||||
:type node_id: str
|
||||
:param loop: 重入次数, -1 表示不过滤重入次数
|
||||
:type loop: int, optional
|
||||
:return: 执行结果
|
||||
:rtype: EngineAPIResult
|
||||
"""
|
||||
return [
|
||||
{
|
||||
"id": h.id,
|
||||
"node_id": h.node_id,
|
||||
"started_time": h.started_time,
|
||||
"archived_time": h.archived_time,
|
||||
"loop": h.loop,
|
||||
"skip": h.skip,
|
||||
"version": h.version,
|
||||
"inputs": h.inputs,
|
||||
"outputs": h.outputs,
|
||||
}
|
||||
for h in runtime.get_histories(node_id, loop)
|
||||
]
|
||||
|
||||
|
||||
@_ensure_return_api_result
|
||||
def get_node_short_histories(runtime: EngineRuntimeInterface, node_id: str, loop: int = -1) -> EngineAPIResult:
|
||||
"""
|
||||
获取某个节点的简要历史记录
|
||||
|
||||
:param runtime: 引擎运行时实例
|
||||
:type runtime: EngineRuntimeInterface
|
||||
:param node_id: 节点 ID
|
||||
:type node_id: str
|
||||
:param loop: 重入次数, -1 表示不过滤重入次数
|
||||
:type loop: int, optional
|
||||
:return: 执行结果
|
||||
:rtype: EngineAPIResult
|
||||
"""
|
||||
return [
|
||||
{
|
||||
"id": h.id,
|
||||
"node_id": h.node_id,
|
||||
"started_time": h.started_time,
|
||||
"archived_time": h.archived_time,
|
||||
"loop": h.loop,
|
||||
"skip": h.skip,
|
||||
"version": h.version,
|
||||
}
|
||||
for h in runtime.get_short_histories(node_id, loop)
|
||||
]
|
||||
|
||||
|
||||
@_ensure_return_api_result
|
||||
def get_pipeline_debug_info(runtime: EngineRuntimeInterface, pipeline_id: str):
|
||||
"""
|
||||
获取某个流程的调试信息
|
||||
|
||||
:param runtime: 引擎运行时实例
|
||||
:type runtime: EngineRuntimeInterface
|
||||
:param pipeline_id: 流程 ID
|
||||
:type pipeline_id: str
|
||||
:return: 执行结果
|
||||
:rtype: EngineAPIResult
|
||||
"""
|
||||
|
||||
return {
|
||||
"contex_values": runtime.get_context(pipeline_id),
|
||||
"processes": runtime.get_process_info_with_root_pipeline(pipeline_id),
|
||||
}
|
||||
|
||||
|
||||
@_ensure_return_api_result
|
||||
def get_node_debug_info(runtime: EngineRuntimeInterface, node_id: str):
|
||||
"""
|
||||
获取某个节点的调试信息
|
||||
|
||||
:param runtime: 引擎运行时实例
|
||||
:type runtime: EngineRuntimeInterface
|
||||
:param node_id: 节点 ID
|
||||
:type node_id: str
|
||||
:return: 执行结果
|
||||
:rtype: EngineAPIResult
|
||||
"""
|
||||
|
||||
data = None
|
||||
state = None
|
||||
err = []
|
||||
|
||||
try:
|
||||
data = runtime.get_data(node_id)
|
||||
except Exception as e:
|
||||
err.append(str(e))
|
||||
|
||||
try:
|
||||
state = runtime.get_state(node_id)
|
||||
except Exception as e:
|
||||
err.append(str(e))
|
||||
|
||||
return {
|
||||
"node": runtime.get_node(node_id),
|
||||
"data": data,
|
||||
"state": state,
|
||||
"err": err,
|
||||
}
|
||||
|
||||
|
||||
@_ensure_return_api_result
|
||||
def preview_node_inputs(
|
||||
runtime: EngineRuntimeInterface,
|
||||
pipeline: dict,
|
||||
node_id: str,
|
||||
subprocess_stack: List[str] = [],
|
||||
root_pipeline_data: dict = {},
|
||||
parent_params: dict = {},
|
||||
):
|
||||
"""
|
||||
预览某个节点的输入结果
|
||||
|
||||
:param pipeline: 预处理后的流程树数据
|
||||
:type pipeline: dict
|
||||
:param node_id: 节点 ID
|
||||
:type node_id: str
|
||||
:param subprocess_stack: 子流程,需保证顺序
|
||||
:type subprocess_stack: List[str]
|
||||
:param root_pipeline_data: root流程数据
|
||||
:param parent_params: 父流程传入参数
|
||||
:return: 执行结果
|
||||
:rtype: EngineAPIResult
|
||||
"""
|
||||
context_values = [
|
||||
ContextValue(key=key, type=VAR_CONTEXT_MAPPING[info["type"]], value=info["value"], code=info.get("custom_type"))
|
||||
for key, info in list(pipeline["data"].get("inputs", {}).items()) + list(parent_params.items())
|
||||
]
|
||||
context = Context(runtime, context_values, root_pipeline_data)
|
||||
|
||||
if subprocess_stack:
|
||||
subprocess = subprocess_stack[0]
|
||||
child_pipeline = pipeline["activities"][subprocess]["pipeline"]
|
||||
param_data = {key: info["value"] for key, info in pipeline["activities"][subprocess]["params"].items()}
|
||||
hydrated_context = context.hydrate(deformat=True)
|
||||
hydrated_param_data = Template(param_data).render(hydrated_context)
|
||||
formatted_param_data = {key: {"value": value, "type": "plain"} for key, value in hydrated_param_data.items()}
|
||||
return preview_node_inputs(
|
||||
runtime=runtime,
|
||||
pipeline=child_pipeline,
|
||||
node_id=node_id,
|
||||
subprocess_stack=subprocess_stack[1:],
|
||||
root_pipeline_data=root_pipeline_data,
|
||||
parent_params=formatted_param_data,
|
||||
)
|
||||
raw_inputs = pipeline["activities"][node_id]["component"]["inputs"]
|
||||
raw_inputs = {key: info["value"] for key, info in raw_inputs.items()}
|
||||
hydrated_context = context.hydrate(deformat=True)
|
||||
inputs = Template(raw_inputs).render(hydrated_context)
|
||||
return inputs
|
||||
15
lib/bamboo_engine/builder/__init__.py
Normal file
15
lib/bamboo_engine/builder/__init__.py
Normal file
@@ -0,0 +1,15 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS Community
|
||||
Edition) available.
|
||||
Copyright (C) 2017-2021 THL A29 Limited, a Tencent company. All rights reserved.
|
||||
Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
http://opensource.org/licenses/MIT
|
||||
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.
|
||||
"""
|
||||
|
||||
from .builder import * # noqa
|
||||
from .flow import * # noqa
|
||||
224
lib/bamboo_engine/builder/builder.py
Normal file
224
lib/bamboo_engine/builder/builder.py
Normal file
@@ -0,0 +1,224 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS Community
|
||||
Edition) available.
|
||||
Copyright (C) 2017-2021 THL A29 Limited, a Tencent company. All rights reserved.
|
||||
Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
http://opensource.org/licenses/MIT
|
||||
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 copy
|
||||
import queue
|
||||
|
||||
from bamboo_engine.utils.string import unique_id
|
||||
|
||||
from .flow.data import Data, Params
|
||||
from .flow.event import ExecutableEndEvent
|
||||
|
||||
|
||||
__all__ = ["build_tree"]
|
||||
|
||||
__skeleton = {
|
||||
"id": None,
|
||||
"start_event": None,
|
||||
"end_event": None,
|
||||
"activities": {},
|
||||
"gateways": {},
|
||||
"flows": {},
|
||||
"data": {"inputs": {}, "outputs": []},
|
||||
}
|
||||
|
||||
__node_type = {
|
||||
"ServiceActivity": "activities",
|
||||
"SubProcess": "activities",
|
||||
"EmptyEndEvent": "end_event",
|
||||
"EmptyStartEvent": "start_event",
|
||||
"ParallelGateway": "gateways",
|
||||
"ConditionalParallelGateway": "gateways",
|
||||
"ExclusiveGateway": "gateways",
|
||||
"ConvergeGateway": "gateways",
|
||||
}
|
||||
|
||||
__start_elem = {"EmptyStartEvent"}
|
||||
|
||||
__end_elem = {"EmptyEndEvent"}
|
||||
|
||||
__multiple_incoming_type = {
|
||||
"ServiceActivity",
|
||||
"ConvergeGateway",
|
||||
"EmptyEndEvent",
|
||||
"ParallelGateway",
|
||||
"ConditionalParallelGateway",
|
||||
"ExclusiveGateway",
|
||||
"SubProcess",
|
||||
}
|
||||
|
||||
__incoming = "__incoming"
|
||||
|
||||
|
||||
def build_tree(start_elem, id=None, data=None):
|
||||
tree = copy.deepcopy(__skeleton)
|
||||
elem_queue = queue.Queue()
|
||||
processed_elem = set()
|
||||
|
||||
tree[__incoming] = {}
|
||||
elem_queue.put(start_elem)
|
||||
|
||||
while not elem_queue.empty():
|
||||
# get elem
|
||||
elem = elem_queue.get()
|
||||
|
||||
# update node when we meet again
|
||||
if elem.id in processed_elem:
|
||||
__update(tree, elem)
|
||||
continue
|
||||
|
||||
# add to queue
|
||||
for e in elem.outgoing:
|
||||
elem_queue.put(e)
|
||||
|
||||
# mark as processed
|
||||
processed_elem.add(elem.id)
|
||||
|
||||
# tree grow
|
||||
__grow(tree, elem)
|
||||
|
||||
del tree[__incoming]
|
||||
tree["id"] = id or unique_id("p")
|
||||
user_data = data.to_dict() if isinstance(data, Data) else data
|
||||
tree["data"] = user_data or tree["data"]
|
||||
return tree
|
||||
|
||||
|
||||
def __update(tree, elem):
|
||||
node_type = __node_type[elem.type()]
|
||||
node = tree[node_type] if node_type == "end_event" else tree[node_type][elem.id]
|
||||
node["incoming"] = tree[__incoming][elem.id]
|
||||
|
||||
|
||||
def __grow(tree, elem):
|
||||
if elem.type() in __start_elem:
|
||||
outgoing = unique_id("f")
|
||||
tree["start_event"] = {
|
||||
"incoming": "",
|
||||
"outgoing": outgoing,
|
||||
"type": elem.type(),
|
||||
"id": elem.id,
|
||||
"name": elem.name,
|
||||
}
|
||||
|
||||
next_elem = elem.outgoing[0]
|
||||
__grow_flow(tree, outgoing, elem, next_elem)
|
||||
|
||||
elif elem.type() in __end_elem or isinstance(elem, ExecutableEndEvent):
|
||||
tree["end_event"] = {
|
||||
"incoming": tree[__incoming][elem.id],
|
||||
"outgoing": "",
|
||||
"type": elem.type(),
|
||||
"id": elem.id,
|
||||
"name": elem.name,
|
||||
}
|
||||
|
||||
elif elem.type() == "ServiceActivity":
|
||||
outgoing = unique_id("f")
|
||||
|
||||
tree["activities"][elem.id] = {
|
||||
"incoming": tree[__incoming][elem.id],
|
||||
"outgoing": outgoing,
|
||||
"type": elem.type(),
|
||||
"id": elem.id,
|
||||
"name": elem.name,
|
||||
"error_ignorable": elem.error_ignorable,
|
||||
"timeout": elem.timeout,
|
||||
"skippable": elem.skippable,
|
||||
"retryable": elem.retryable,
|
||||
"component": elem.component_dict(),
|
||||
"optional": False,
|
||||
}
|
||||
|
||||
next_elem = elem.outgoing[0]
|
||||
__grow_flow(tree, outgoing, elem, next_elem)
|
||||
|
||||
elif elem.type() == "SubProcess":
|
||||
outgoing = unique_id("f")
|
||||
|
||||
subprocess_param = elem.params.to_dict() if isinstance(elem.params, Params) else elem.params
|
||||
|
||||
subprocess = {
|
||||
"id": elem.id,
|
||||
"incoming": tree[__incoming][elem.id],
|
||||
"name": elem.name,
|
||||
"outgoing": outgoing,
|
||||
"type": elem.type(),
|
||||
"params": subprocess_param,
|
||||
}
|
||||
|
||||
subprocess["pipeline"] = build_tree(start_elem=elem.start, id=elem.id, data=elem.data)
|
||||
|
||||
tree["activities"][elem.id] = subprocess
|
||||
|
||||
next_elem = elem.outgoing[0]
|
||||
__grow_flow(tree, outgoing, elem, next_elem)
|
||||
|
||||
elif elem.type() == "ParallelGateway":
|
||||
outgoing = [unique_id("f") for _ in range(len(elem.outgoing))]
|
||||
|
||||
tree["gateways"][elem.id] = {
|
||||
"id": elem.id,
|
||||
"incoming": tree[__incoming][elem.id],
|
||||
"outgoing": outgoing,
|
||||
"type": elem.type(),
|
||||
"name": elem.name,
|
||||
}
|
||||
|
||||
for i, next_elem in enumerate(elem.outgoing):
|
||||
__grow_flow(tree, outgoing[i], elem, next_elem)
|
||||
|
||||
elif elem.type() in {"ExclusiveGateway", "ConditionalParallelGateway"}:
|
||||
outgoing = [unique_id("f") for _ in range(len(elem.outgoing))]
|
||||
|
||||
tree["gateways"][elem.id] = {
|
||||
"id": elem.id,
|
||||
"incoming": tree[__incoming][elem.id],
|
||||
"outgoing": outgoing,
|
||||
"type": elem.type(),
|
||||
"name": elem.name,
|
||||
"conditions": elem.link_conditions_with(outgoing),
|
||||
}
|
||||
|
||||
for i, next_elem in enumerate(elem.outgoing):
|
||||
__grow_flow(tree, outgoing[i], elem, next_elem)
|
||||
|
||||
elif elem.type() == "ConvergeGateway":
|
||||
outgoing = unique_id("f")
|
||||
|
||||
tree["gateways"][elem.id] = {
|
||||
"id": elem.id,
|
||||
"incoming": tree[__incoming][elem.id],
|
||||
"outgoing": outgoing,
|
||||
"type": elem.type(),
|
||||
"name": elem.name,
|
||||
}
|
||||
|
||||
next_elem = elem.outgoing[0]
|
||||
__grow_flow(tree, outgoing, elem, next_elem)
|
||||
|
||||
else:
|
||||
raise Exception()
|
||||
|
||||
|
||||
def __grow_flow(tree, outgoing, elem, next_element):
|
||||
tree["flows"][outgoing] = {
|
||||
"is_default": False,
|
||||
"source": elem.id,
|
||||
"target": next_element.id,
|
||||
"id": outgoing,
|
||||
}
|
||||
if next_element.type() in __multiple_incoming_type:
|
||||
tree[__incoming].setdefault(next_element.id, []).append(outgoing)
|
||||
else:
|
||||
tree[__incoming][next_element.id] = outgoing
|
||||
17
lib/bamboo_engine/builder/flow/__init__.py
Normal file
17
lib/bamboo_engine/builder/flow/__init__.py
Normal file
@@ -0,0 +1,17 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS Community
|
||||
Edition) available.
|
||||
Copyright (C) 2017-2021 THL A29 Limited, a Tencent company. All rights reserved.
|
||||
Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
http://opensource.org/licenses/MIT
|
||||
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.
|
||||
"""
|
||||
|
||||
from .activity import * # noqa
|
||||
from .event import * # noqa
|
||||
from .gateway import * # noqa
|
||||
from .data import * # noqa
|
||||
51
lib/bamboo_engine/builder/flow/activity.py
Normal file
51
lib/bamboo_engine/builder/flow/activity.py
Normal file
@@ -0,0 +1,51 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS Community
|
||||
Edition) available.
|
||||
Copyright (C) 2017-2021 THL A29 Limited, a Tencent company. All rights reserved.
|
||||
Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
http://opensource.org/licenses/MIT
|
||||
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.
|
||||
"""
|
||||
|
||||
from .base import Element
|
||||
from bamboo_engine.eri import NodeType
|
||||
from bamboo_engine.utils.collections import FancyDict
|
||||
|
||||
__all__ = ["ServiceActivity", "SubProcess"]
|
||||
|
||||
|
||||
class ServiceActivity(Element):
|
||||
def __init__(
|
||||
self, component_code=None, error_ignorable=False, timeout=None, skippable=True, retryable=True, *args, **kwargs
|
||||
):
|
||||
self.component = FancyDict({"code": component_code, "inputs": FancyDict({})})
|
||||
self.error_ignorable = error_ignorable
|
||||
self.timeout = timeout
|
||||
self.skippable = skippable
|
||||
self.retryable = retryable
|
||||
super(ServiceActivity, self).__init__(*args, **kwargs)
|
||||
|
||||
def type(self):
|
||||
return NodeType.ServiceActivity.value
|
||||
|
||||
def component_dict(self):
|
||||
return {
|
||||
"code": self.component.code,
|
||||
"inputs": {key: var.to_dict() for key, var in list(self.component.inputs.items())},
|
||||
}
|
||||
|
||||
|
||||
class SubProcess(Element):
|
||||
def __init__(self, start=None, data=None, params=None, global_outputs=None, *args, **kwargs):
|
||||
self.start = start
|
||||
self.data = data
|
||||
self.params = params or {}
|
||||
self.global_outputs = FancyDict(global_outputs or {})
|
||||
super(SubProcess, self).__init__(*args, **kwargs)
|
||||
|
||||
def type(self):
|
||||
return NodeType.SubProcess.value
|
||||
78
lib/bamboo_engine/builder/flow/base.py
Normal file
78
lib/bamboo_engine/builder/flow/base.py
Normal file
@@ -0,0 +1,78 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS Community
|
||||
Edition) available.
|
||||
Copyright (C) 2017-2021 THL A29 Limited, a Tencent company. All rights reserved.
|
||||
Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
http://opensource.org/licenses/MIT
|
||||
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.
|
||||
"""
|
||||
|
||||
from bamboo_engine.utils.string import unique_id
|
||||
|
||||
__all__ = ["Element"]
|
||||
|
||||
|
||||
class Element(object):
|
||||
def __init__(self, id=None, name=None, outgoing=None):
|
||||
self.id = id or unique_id("e")
|
||||
self.name = name
|
||||
self.outgoing = outgoing or []
|
||||
|
||||
def extend(self, element):
|
||||
"""
|
||||
build a connection from self to element and return element
|
||||
:param element: target
|
||||
:rtype: Element
|
||||
"""
|
||||
self.outgoing.append(element)
|
||||
return element
|
||||
|
||||
def connect(self, *args):
|
||||
"""
|
||||
build connections from self to elements in args and return self
|
||||
:param args: target elements
|
||||
:rtype: Element
|
||||
"""
|
||||
for e in args:
|
||||
self.outgoing.append(e)
|
||||
return self
|
||||
|
||||
def converge(self, element):
|
||||
"""
|
||||
converge all connection those diverge from self to element and return element
|
||||
:param element: target
|
||||
:rtype: Element
|
||||
"""
|
||||
for e in self.outgoing:
|
||||
e.tail().connect(element)
|
||||
return element
|
||||
|
||||
def to(self, element):
|
||||
return element
|
||||
|
||||
def tail(self):
|
||||
"""
|
||||
get tail element for self
|
||||
:rtype: Element
|
||||
"""
|
||||
is_tail = len(self.outgoing) == 0
|
||||
e = self
|
||||
|
||||
while not is_tail:
|
||||
e = e.outgoing[0]
|
||||
is_tail = len(e.outgoing) == 0
|
||||
|
||||
return e
|
||||
|
||||
def type(self):
|
||||
raise NotImplementedError()
|
||||
|
||||
def __eq__(self, other):
|
||||
return self.id == other.id
|
||||
|
||||
def __repr__(self):
|
||||
return "<{cls} {name}:{id}>".format(cls=type(self).__name__, name=self.name, id=self.id)
|
||||
96
lib/bamboo_engine/builder/flow/data.py
Normal file
96
lib/bamboo_engine/builder/flow/data.py
Normal file
@@ -0,0 +1,96 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS Community
|
||||
Edition) available.
|
||||
Copyright (C) 2017-2021 THL A29 Limited, a Tencent company. All rights reserved.
|
||||
Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
http://opensource.org/licenses/MIT
|
||||
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.
|
||||
"""
|
||||
|
||||
from bamboo_engine.utils.collections import FancyDict
|
||||
|
||||
|
||||
class Data(object):
|
||||
def __init__(self, inputs=None, outputs=None, pre_render_keys=None):
|
||||
self.inputs = FancyDict(inputs or {})
|
||||
self.outputs = outputs or []
|
||||
self.pre_render_keys = pre_render_keys or []
|
||||
|
||||
def to_dict(self):
|
||||
base = {"inputs": {}, "outputs": self.outputs, "pre_render_keys": self.pre_render_keys}
|
||||
|
||||
for key, value in list(self.inputs.items()):
|
||||
base["inputs"][key] = value.to_dict() if isinstance(value, Var) else value
|
||||
|
||||
return base
|
||||
|
||||
|
||||
class Params(object):
|
||||
def __init__(self, params=None):
|
||||
self.params = FancyDict(params or {})
|
||||
|
||||
def to_dict(self):
|
||||
base = {}
|
||||
|
||||
for key, value in list(self.params.items()):
|
||||
base[key] = value.to_dict() if isinstance(value, Var) else value
|
||||
|
||||
return base
|
||||
|
||||
|
||||
class Var(object):
|
||||
PLAIN = "plain"
|
||||
SPLICE = "splice"
|
||||
LAZY = "lazy"
|
||||
|
||||
def __init__(self, type, value, custom_type=None):
|
||||
self.type = type
|
||||
self.value = value
|
||||
self.custom_type = custom_type
|
||||
|
||||
def to_dict(self):
|
||||
base = {"type": self.type, "value": self.value}
|
||||
if self.type == self.LAZY:
|
||||
base["custom_type"] = self.custom_type
|
||||
|
||||
return base
|
||||
|
||||
|
||||
class DataInput(Var):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(DataInput, self).__init__(*args, **kwargs)
|
||||
|
||||
def to_dict(self):
|
||||
base = super(DataInput, self).to_dict()
|
||||
base["is_param"] = True
|
||||
return base
|
||||
|
||||
|
||||
class NodeOutput(Var):
|
||||
def __init__(self, source_act, source_key, *args, **kwargs):
|
||||
self.source_act = source_act
|
||||
self.source_key = source_key
|
||||
kwargs["value"] = None
|
||||
super(NodeOutput, self).__init__(*args, **kwargs)
|
||||
|
||||
def to_dict(self):
|
||||
base = super(NodeOutput, self).to_dict()
|
||||
base["source_act"] = self.source_act
|
||||
base["source_key"] = self.source_key
|
||||
return base
|
||||
|
||||
|
||||
class RewritableNodeOutput(Var):
|
||||
def __init__(self, source_act, *args, **kwargs):
|
||||
self.source_act = source_act
|
||||
kwargs["value"] = None
|
||||
super(RewritableNodeOutput, self).__init__(*args, **kwargs)
|
||||
|
||||
def to_dict(self):
|
||||
base = super(RewritableNodeOutput, self).to_dict()
|
||||
base["source_act"] = self.source_act
|
||||
return base
|
||||
35
lib/bamboo_engine/builder/flow/event.py
Normal file
35
lib/bamboo_engine/builder/flow/event.py
Normal file
@@ -0,0 +1,35 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS Community
|
||||
Edition) available.
|
||||
Copyright (C) 2017-2021 THL A29 Limited, a Tencent company. All rights reserved.
|
||||
Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
http://opensource.org/licenses/MIT
|
||||
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.
|
||||
"""
|
||||
from bamboo_engine.eri import NodeType
|
||||
from .base import Element
|
||||
|
||||
__all__ = ["EmptyEndEvent", "EmptyStartEvent", "ExecutableEndEvent"]
|
||||
|
||||
|
||||
class EmptyStartEvent(Element):
|
||||
def type(self):
|
||||
return NodeType.EmptyStartEvent.value
|
||||
|
||||
|
||||
class EmptyEndEvent(Element):
|
||||
def type(self):
|
||||
return NodeType.EmptyEndEvent.value
|
||||
|
||||
|
||||
class ExecutableEndEvent(Element):
|
||||
def __init__(self, type, **kwargs):
|
||||
self._type = type
|
||||
super(ExecutableEndEvent, self).__init__(**kwargs)
|
||||
|
||||
def type(self):
|
||||
return self._type
|
||||
59
lib/bamboo_engine/builder/flow/gateway.py
Normal file
59
lib/bamboo_engine/builder/flow/gateway.py
Normal file
@@ -0,0 +1,59 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS Community
|
||||
Edition) available.
|
||||
Copyright (C) 2017-2021 THL A29 Limited, a Tencent company. All rights reserved.
|
||||
Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
http://opensource.org/licenses/MIT
|
||||
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.
|
||||
"""
|
||||
|
||||
from bamboo_engine.eri import NodeType
|
||||
|
||||
from .base import Element
|
||||
|
||||
__all__ = [
|
||||
"ParallelGateway",
|
||||
"ExclusiveGateway",
|
||||
"ConvergeGateway",
|
||||
"ConditionalParallelGateway",
|
||||
]
|
||||
|
||||
|
||||
class ParallelGateway(Element):
|
||||
def type(self):
|
||||
return NodeType.ParallelGateway.value
|
||||
|
||||
|
||||
class ConditionGateway(Element):
|
||||
def __init__(self, conditions=None, *args, **kwargs):
|
||||
self.conditions = conditions or {}
|
||||
super(ConditionGateway, self).__init__(*args, **kwargs)
|
||||
|
||||
def add_condition(self, index, evaluate):
|
||||
self.conditions[index] = evaluate
|
||||
|
||||
def link_conditions_with(self, outgoing):
|
||||
conditions = {}
|
||||
for i, out in enumerate(outgoing):
|
||||
conditions[out] = {"evaluate": self.conditions[i]}
|
||||
|
||||
return conditions
|
||||
|
||||
|
||||
class ConditionalParallelGateway(ConditionGateway):
|
||||
def type(self):
|
||||
return NodeType.ConditionalParallelGateway.value
|
||||
|
||||
|
||||
class ExclusiveGateway(ConditionGateway):
|
||||
def type(self):
|
||||
return NodeType.ExclusiveGateway.value
|
||||
|
||||
|
||||
class ConvergeGateway(Element):
|
||||
def type(self):
|
||||
return NodeType.ConvergeGateway.value
|
||||
66
lib/bamboo_engine/config.py
Normal file
66
lib/bamboo_engine/config.py
Normal file
@@ -0,0 +1,66 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS Community
|
||||
Edition) available.
|
||||
Copyright (C) 2017-2021 THL A29 Limited, a Tencent company. All rights reserved.
|
||||
Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
http://opensource.org/licenses/MIT
|
||||
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.
|
||||
"""
|
||||
|
||||
# 引擎内部配置模块
|
||||
|
||||
|
||||
class Settings:
|
||||
"""
|
||||
引擎全局配置对象
|
||||
"""
|
||||
|
||||
MAKO_SANDBOX_SHIELD_WORDS = [
|
||||
"ascii",
|
||||
"bytearray",
|
||||
"bytes",
|
||||
"callable",
|
||||
"chr",
|
||||
"classmethod",
|
||||
"compile",
|
||||
"delattr",
|
||||
"dir",
|
||||
"divmod",
|
||||
"exec",
|
||||
"eval",
|
||||
"filter",
|
||||
"frozenset",
|
||||
"getattr",
|
||||
"globals",
|
||||
"hasattr",
|
||||
"hash",
|
||||
"help",
|
||||
"id",
|
||||
"input",
|
||||
"isinstance",
|
||||
"issubclass",
|
||||
"iter",
|
||||
"locals",
|
||||
"map",
|
||||
"memoryview",
|
||||
"next",
|
||||
"object",
|
||||
"open",
|
||||
"print",
|
||||
"property",
|
||||
"repr",
|
||||
"setattr",
|
||||
"staticmethod",
|
||||
"super",
|
||||
"type",
|
||||
"vars",
|
||||
"__import__",
|
||||
]
|
||||
|
||||
MAKO_SANDBOX_IMPORT_MODULES = {}
|
||||
|
||||
RERUN_INDEX_OFFSET = 0
|
||||
169
lib/bamboo_engine/context.py
Normal file
169
lib/bamboo_engine/context.py
Normal file
@@ -0,0 +1,169 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS Community
|
||||
Edition) available.
|
||||
Copyright (C) 2017-2021 THL A29 Limited, a Tencent company. All rights reserved.
|
||||
Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
http://opensource.org/licenses/MIT
|
||||
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 logging
|
||||
from weakref import WeakValueDictionary
|
||||
from typing import List, Dict, Any
|
||||
|
||||
from bamboo_engine.eri import (
|
||||
ContextValue,
|
||||
EngineRuntimeInterface,
|
||||
Variable,
|
||||
ContextValueType,
|
||||
)
|
||||
from .template.template import Template
|
||||
from .utils.string import deformat_var_key
|
||||
|
||||
logger = logging.getLogger("bamboo_engine")
|
||||
|
||||
|
||||
class PlainVariable(Variable):
|
||||
"""
|
||||
普通变量
|
||||
"""
|
||||
|
||||
def __init__(self, key: str, value: Any):
|
||||
self.key = key
|
||||
self.value = value
|
||||
|
||||
def get(self):
|
||||
return self.value
|
||||
|
||||
|
||||
class SpliceVariable(Variable):
|
||||
"""
|
||||
模板类型变量,会尝试在流程上下文中解析变量中定义的模板
|
||||
"""
|
||||
|
||||
def __init__(self, key: str, value: Any, pool: WeakValueDictionary):
|
||||
self.key = key
|
||||
self.value = value
|
||||
self.pool = pool
|
||||
self.refs = [k for k in Template(value).get_reference()]
|
||||
|
||||
def get(self):
|
||||
context = {}
|
||||
for r in self.refs:
|
||||
if r not in self.pool:
|
||||
continue
|
||||
|
||||
var = self.pool[r]
|
||||
if issubclass(var.__class__, Variable):
|
||||
var = var.get()
|
||||
context[deformat_var_key(r)] = var
|
||||
|
||||
return Template(self.value).render(context=context)
|
||||
|
||||
|
||||
def _raw_key(key: str) -> str:
|
||||
return key
|
||||
|
||||
|
||||
class Context:
|
||||
"""
|
||||
流程执行上下文,封装引擎在执行流程的过程中对上下文进行的操作和逻辑
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
runtime: EngineRuntimeInterface,
|
||||
values: List[ContextValue],
|
||||
additional_data: dict,
|
||||
):
|
||||
"""
|
||||
|
||||
:param runtime: 引擎运行时实例
|
||||
:type runtime: EngineRuntimeInterface
|
||||
:param values: 上下文数据列表
|
||||
:type values: List[ContextValue]
|
||||
:param additional_data: 额外数据字典
|
||||
:type additional_data: dict
|
||||
"""
|
||||
self.values = values
|
||||
self.runtime = runtime
|
||||
self.pool = WeakValueDictionary()
|
||||
self.variables = {}
|
||||
self.additional_data = additional_data
|
||||
|
||||
# 将上下文数据转换成变量,变量内封装了自身解析的逻辑,且实现了 Variable 接口
|
||||
for v in self.values:
|
||||
if v.type is ContextValueType.PLAIN:
|
||||
self.variables[v.key] = PlainVariable(key=v.key, value=v.value)
|
||||
elif v.type is ContextValueType.SPLICE:
|
||||
self.variables[v.key] = SpliceVariable(key=v.key, value=v.value, pool=self.pool)
|
||||
elif v.type is ContextValueType.COMPUTE:
|
||||
self.variables[v.key] = self.runtime.get_compute_variable(
|
||||
code=v.code,
|
||||
key=v.key,
|
||||
value=SpliceVariable(key=v.key, value=v.value, pool=self.pool),
|
||||
additional_data=self.additional_data,
|
||||
)
|
||||
|
||||
for k, var in self.variables.items():
|
||||
self.pool[k] = var
|
||||
|
||||
def hydrate(self, deformat=False, mute_error=False) -> Dict[str, Any]:
|
||||
"""
|
||||
将当前上下文中的数据清洗成 Dict[str, Any] 类型的朴素数据,过程中会进行变量引用的分析和替换
|
||||
|
||||
:param deformat: 是否将返回字典中的 key 值从 ${%s} 替换为 %s
|
||||
:type deformat: bool, optional
|
||||
:return: 上下文数据朴素值字典
|
||||
:rtype: Dict[str, Any]
|
||||
"""
|
||||
key_formatter = deformat_var_key if deformat else _raw_key
|
||||
hydrated = {}
|
||||
|
||||
for key, var in self.pool.items():
|
||||
try:
|
||||
hydrated[key_formatter(key)] = var.get()
|
||||
except Exception as e:
|
||||
if not mute_error:
|
||||
raise e
|
||||
logger.exception("%s get error." % key)
|
||||
hydrated[key_formatter(key)] = str(e)
|
||||
|
||||
return hydrated
|
||||
|
||||
def extract_outputs(
|
||||
self,
|
||||
pipeline_id: str,
|
||||
data_outputs: Dict[str, str],
|
||||
execution_data_outputs: Dict[str, Any],
|
||||
):
|
||||
"""
|
||||
将某个节点的输出提取到流程上下文中
|
||||
|
||||
:param pipeline_id: 上下文对应的流程/子流程 ID
|
||||
:type pipeline_id: str
|
||||
:param data_outputs: 节点输出键映射
|
||||
:type data_outputs: Dict[str, str]
|
||||
:param execution_data_outputs: 节点执行数据输出
|
||||
:type execution_data_outputs: Dict[str, Any]
|
||||
"""
|
||||
update = {}
|
||||
for origin_key, target_key in data_outputs.items():
|
||||
if origin_key not in execution_data_outputs:
|
||||
continue
|
||||
|
||||
update[target_key] = ContextValue(
|
||||
key=target_key,
|
||||
type=ContextValueType.PLAIN,
|
||||
value=execution_data_outputs[origin_key],
|
||||
)
|
||||
|
||||
self.runtime.upsert_plain_context_values(pipeline_id=pipeline_id, update=update)
|
||||
1069
lib/bamboo_engine/engine.py
Normal file
1069
lib/bamboo_engine/engine.py
Normal file
File diff suppressed because it is too large
Load Diff
19
lib/bamboo_engine/eri/__init__.py
Normal file
19
lib/bamboo_engine/eri/__init__.py
Normal file
@@ -0,0 +1,19 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS Community
|
||||
Edition) available.
|
||||
Copyright (C) 2017-2021 THL A29 Limited, a Tencent company. All rights reserved.
|
||||
Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
http://opensource.org/licenses/MIT
|
||||
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.
|
||||
"""
|
||||
|
||||
"""
|
||||
引擎运行时接口定义模块
|
||||
"""
|
||||
|
||||
from .interfaces import * # noqa
|
||||
from .models import * # noqa
|
||||
1433
lib/bamboo_engine/eri/interfaces.py
Normal file
1433
lib/bamboo_engine/eri/interfaces.py
Normal file
File diff suppressed because it is too large
Load Diff
646
lib/bamboo_engine/eri/models.py
Normal file
646
lib/bamboo_engine/eri/models.py
Normal file
@@ -0,0 +1,646 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS Community
|
||||
Edition) available.
|
||||
Copyright (C) 2017-2021 THL A29 Limited, a Tencent company. All rights reserved.
|
||||
Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
http://opensource.org/licenses/MIT
|
||||
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.
|
||||
"""
|
||||
|
||||
|
||||
# ERI 中相关的模型对象
|
||||
|
||||
|
||||
from enum import Enum
|
||||
from datetime import datetime
|
||||
from typing import List, Dict, Any, Optional
|
||||
|
||||
from bamboo_engine.utils.object import Representable
|
||||
from bamboo_engine.utils.collections import FancyDict
|
||||
from bamboo_engine.exceptions import ValueError
|
||||
|
||||
|
||||
# node relate models
|
||||
class NodeType(Enum):
|
||||
"""
|
||||
节点类型枚举
|
||||
"""
|
||||
|
||||
ServiceActivity = "ServiceActivity"
|
||||
SubProcess = "SubProcess"
|
||||
ExclusiveGateway = "ExclusiveGateway"
|
||||
ParallelGateway = "ParallelGateway"
|
||||
ConditionalParallelGateway = "ConditionalParallelGateway"
|
||||
ConvergeGateway = "ConvergeGateway"
|
||||
EmptyStartEvent = "EmptyStartEvent"
|
||||
EmptyEndEvent = "EmptyEndEvent"
|
||||
ExecutableEndEvent = "ExecutableEndEvent"
|
||||
|
||||
|
||||
class Node(Representable):
|
||||
"""
|
||||
节点信息描述类
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
id: str,
|
||||
type: NodeType,
|
||||
target_flows: List[str],
|
||||
target_nodes: List[str],
|
||||
targets: Dict[str, str],
|
||||
root_pipeline_id: str,
|
||||
parent_pipeline_id: str,
|
||||
can_skip: bool = True,
|
||||
can_retry: bool = True,
|
||||
):
|
||||
"""
|
||||
|
||||
:param id: 节点 ID
|
||||
:type id: str
|
||||
:param type: 节点类型
|
||||
:type type: NodeType
|
||||
:param target_flows: 节点目标流 ID 列表
|
||||
:type target_flows: List[str]
|
||||
:param target_nodes: 目标节点 ID 列表
|
||||
:type target_nodes: List[str]
|
||||
:param targets: 节点目标流,目标节点 ID 映射
|
||||
:type targets: Dict[str, str]
|
||||
:param root_pipeline_id: 根流程 ID
|
||||
:type root_pipeline_id: str
|
||||
:param parent_pipeline_id: 父流程 ID
|
||||
:type parent_pipeline_id: str
|
||||
:param can_skip: 节点是否能够跳过
|
||||
:type can_skip: bool
|
||||
:param can_retry: 节点是否能够重试
|
||||
:type can_retry: bool
|
||||
"""
|
||||
self.id = id
|
||||
self.type = type
|
||||
self.targets = targets
|
||||
self.target_flows = target_flows
|
||||
self.target_nodes = target_nodes
|
||||
self.root_pipeline_id = root_pipeline_id
|
||||
self.parent_pipeline_id = parent_pipeline_id
|
||||
self.can_skip = can_skip
|
||||
self.can_retry = can_retry
|
||||
|
||||
|
||||
class EmptyStartEvent(Node):
|
||||
pass
|
||||
|
||||
|
||||
class ConvergeGateway(Node):
|
||||
pass
|
||||
|
||||
|
||||
class EmptyEndEvent(Node):
|
||||
pass
|
||||
|
||||
|
||||
class Condition(Representable):
|
||||
"""
|
||||
分支条件
|
||||
"""
|
||||
|
||||
def __init__(self, name: str, evaluation: str, target_id: str, flow_id: str):
|
||||
"""
|
||||
|
||||
:param name: 条件名
|
||||
:type name: str
|
||||
:param evaluation: 条件表达式
|
||||
:type evaluation: str
|
||||
:param target_id: 目标节点 ID
|
||||
:type target_id: str
|
||||
:param flow_id: 目标流 ID
|
||||
:type flow_id: str
|
||||
"""
|
||||
self.name = name
|
||||
self.evaluation = evaluation
|
||||
self.target_id = target_id
|
||||
self.flow_id = flow_id
|
||||
|
||||
|
||||
class ParallelGateway(Node):
|
||||
"""
|
||||
并行网关
|
||||
"""
|
||||
|
||||
def __init__(self, converge_gateway_id: str, *args, **kwargs):
|
||||
"""
|
||||
|
||||
:param converge_gateway_id: 汇聚网关 ID
|
||||
:type converge_gateway_id: str
|
||||
"""
|
||||
super().__init__(*args, **kwargs)
|
||||
self.converge_gateway_id = converge_gateway_id
|
||||
|
||||
|
||||
class ConditionalParallelGateway(Node):
|
||||
"""
|
||||
条件并行网关
|
||||
"""
|
||||
|
||||
def __init__(self, conditions: List[Condition], converge_gateway_id: str, *args, **kwargs):
|
||||
"""
|
||||
|
||||
:param conditions: 分支条件
|
||||
:type conditions: List[Condition]
|
||||
:param converge_gateway_id: 汇聚网关 ID
|
||||
:type converge_gateway_id: str
|
||||
"""
|
||||
super().__init__(*args, **kwargs)
|
||||
self.conditions = conditions
|
||||
self.converge_gateway_id = converge_gateway_id
|
||||
|
||||
|
||||
class ExclusiveGateway(Node):
|
||||
"""
|
||||
分支网关
|
||||
"""
|
||||
|
||||
def __init__(self, conditions: List[Condition], *args, **kwargs):
|
||||
"""
|
||||
|
||||
:param conditions: 分支条件
|
||||
:type conditions: List[Condition]
|
||||
"""
|
||||
super().__init__(*args, **kwargs)
|
||||
self.conditions = conditions
|
||||
|
||||
|
||||
class ServiceActivity(Node):
|
||||
"""
|
||||
服务节点
|
||||
"""
|
||||
|
||||
def __init__(self, code: str, version: str, timeout: Optional[int], error_ignorable: bool, *args, **kwargs):
|
||||
"""
|
||||
|
||||
:param code: Service Code
|
||||
:type code: str
|
||||
:param version: 版本
|
||||
:type version: str
|
||||
:param timeout: 超时限制
|
||||
:type timeout: Optional[int]
|
||||
:param error_ignorable: 是否忽略错误
|
||||
:type error_ignorable: bool
|
||||
"""
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
self.code = code
|
||||
self.version = version
|
||||
self.timeout = timeout
|
||||
self.error_ignorable = error_ignorable
|
||||
|
||||
|
||||
class SubProcess(Node):
|
||||
"""
|
||||
子流程
|
||||
"""
|
||||
|
||||
def __init__(self, start_event_id: str, *args, **kwargs):
|
||||
"""
|
||||
|
||||
:param start_event_id: 子流程开始节点 ID
|
||||
:type start_event_id: str
|
||||
"""
|
||||
super().__init__(*args, **kwargs)
|
||||
self.start_event_id = start_event_id
|
||||
|
||||
|
||||
class ExecutableEndEvent(Node):
|
||||
"""
|
||||
可执行结束节点
|
||||
"""
|
||||
|
||||
def __init__(self, code: str, *args, **kwargs):
|
||||
"""
|
||||
|
||||
:param code: 可执行结束节点 ID
|
||||
:type code: str
|
||||
"""
|
||||
super().__init__(*args, **kwargs)
|
||||
self.code = code
|
||||
|
||||
|
||||
# runtime relate models
|
||||
class ScheduleType(Enum):
|
||||
"""
|
||||
调度类型
|
||||
"""
|
||||
|
||||
CALLBACK = 1
|
||||
MULTIPLE_CALLBACK = 2
|
||||
POLL = 3
|
||||
|
||||
|
||||
class Schedule(Representable):
|
||||
"""
|
||||
调度对象
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
id: int,
|
||||
type: ScheduleType,
|
||||
process_id: int,
|
||||
node_id: str,
|
||||
finished: bool,
|
||||
expired: bool,
|
||||
version: str,
|
||||
times: int,
|
||||
):
|
||||
"""
|
||||
|
||||
:param id: ID
|
||||
:type id: int
|
||||
:param type: 类型
|
||||
:type type: ScheduleType
|
||||
:param process_id: 进程 ID
|
||||
:type process_id: int
|
||||
:param node_id: 节点 ID
|
||||
:type node_id: str
|
||||
:param finished: 是否已完成
|
||||
:type finished: bool
|
||||
:param expired: 是否已过期
|
||||
:type expired: bool
|
||||
:param version: 绑定版本
|
||||
:type version: str
|
||||
:param times: 调度次数
|
||||
:type times: int
|
||||
"""
|
||||
self.id = id
|
||||
self.type = type
|
||||
self.process_id = process_id
|
||||
self.node_id = node_id
|
||||
self.finished = finished
|
||||
self.expired = expired
|
||||
self.version = version
|
||||
self.times = times
|
||||
|
||||
|
||||
class State(Representable):
|
||||
"""
|
||||
节点状态对象
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
node_id: str,
|
||||
root_id: str,
|
||||
parent_id: str,
|
||||
name: str,
|
||||
version: str,
|
||||
loop: int,
|
||||
inner_loop: int,
|
||||
retry: int,
|
||||
skip: bool,
|
||||
error_ignored: bool,
|
||||
created_time: datetime,
|
||||
started_time: datetime,
|
||||
archived_time: datetime,
|
||||
):
|
||||
"""
|
||||
:param node_id: 节点 ID
|
||||
:type node_id: str
|
||||
:param root_id: 根流程 ID
|
||||
:type root_id: str
|
||||
:param parent_id: 父流程 ID
|
||||
:type parent_id: str
|
||||
:param name: 状态名
|
||||
:type name: str
|
||||
:param version: 版本
|
||||
:type version: str
|
||||
:param loop: 重入次数
|
||||
:type loop: int
|
||||
:param inner_loop: 子流程重入次数
|
||||
:type inner_loop: int
|
||||
:param retry: 重试次数
|
||||
:type retry: int
|
||||
:param skip: 是否跳过
|
||||
:type skip: bool
|
||||
:param error_ignored: 是否出错后自动忽略
|
||||
:type error_ignored: bool
|
||||
:param started_time: 创建时间
|
||||
:type started_time: datetime
|
||||
:param started_time: 开始时间
|
||||
:type started_time: datetime
|
||||
:param archived_time: 归档时间
|
||||
:type archived_time: datetime
|
||||
"""
|
||||
self.node_id = node_id
|
||||
self.root_id = root_id
|
||||
self.parent_id = parent_id
|
||||
self.name = name
|
||||
self.version = version
|
||||
self.loop = loop
|
||||
self.inner_loop = inner_loop
|
||||
self.retry = retry
|
||||
self.skip = skip
|
||||
self.error_ignored = error_ignored
|
||||
self.created_time = created_time
|
||||
self.started_time = started_time
|
||||
self.archived_time = archived_time
|
||||
|
||||
|
||||
class DataInput(Representable):
|
||||
"""
|
||||
节点数据输入项
|
||||
"""
|
||||
|
||||
def __init__(self, need_render: bool, value: Any):
|
||||
"""
|
||||
:type is_splice: bool
|
||||
:param value: 是否需要进行模板解析
|
||||
:type value: Any
|
||||
"""
|
||||
self.need_render = need_render
|
||||
self.value = value
|
||||
|
||||
|
||||
class Data(Representable):
|
||||
"""
|
||||
节点数据对象
|
||||
"""
|
||||
|
||||
def __init__(self, inputs: Dict[str, DataInput], outputs: Dict[str, str]):
|
||||
"""
|
||||
|
||||
:param inputs: 输入数据
|
||||
:type inputs: Dict[str, Any]
|
||||
:param outputs: 节点输出配置
|
||||
:type outputs: Dict[str, str]
|
||||
"""
|
||||
self.inputs = inputs
|
||||
self.outputs = outputs
|
||||
|
||||
def plain_inputs(self) -> Dict[str, Any]:
|
||||
"""
|
||||
获取不带输入项类型的输入字典
|
||||
"""
|
||||
return {key: di.value for key, di in self.inputs.items()}
|
||||
|
||||
def need_render_inputs(self) -> Dict[str, Any]:
|
||||
"""
|
||||
获取需要进行渲染的输入项字典
|
||||
"""
|
||||
return {key: di.value for key, di in self.inputs.items() if di.need_render}
|
||||
|
||||
def render_escape_inputs(self) -> Dict[str, Any]:
|
||||
"""
|
||||
获取不需要进行渲染的输入项字典
|
||||
"""
|
||||
return {key: di.value for key, di in self.inputs.items() if not di.need_render}
|
||||
|
||||
|
||||
class ExecutionData(Representable):
|
||||
"""
|
||||
节点输出数据
|
||||
"""
|
||||
|
||||
def __init__(self, inputs: Optional[dict], outputs: Optional[dict]):
|
||||
"""
|
||||
|
||||
:param inputs: 输入数据
|
||||
:type inputs: Optional[dict]
|
||||
:param outputs: 输出数据
|
||||
:type outputs: Optional[dict]
|
||||
"""
|
||||
self.inputs = FancyDict(inputs)
|
||||
self.outputs = FancyDict(outputs)
|
||||
|
||||
|
||||
class ExecutionHistory(Representable):
|
||||
"""
|
||||
节点执行历史
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
id: str,
|
||||
node_id: str,
|
||||
started_time: datetime,
|
||||
archived_time: datetime,
|
||||
loop: int,
|
||||
skip: bool,
|
||||
retry: int,
|
||||
version: str,
|
||||
inputs: dict,
|
||||
outputs: dict,
|
||||
):
|
||||
"""
|
||||
|
||||
: param id: ID
|
||||
: type id: str
|
||||
: param node_id: Node ID
|
||||
: type node_id: str
|
||||
: param started_time: 开始时间
|
||||
: type started_time: datetime
|
||||
: param archived_time: 归档时间
|
||||
: type archived_time: datetime
|
||||
: param loop: 重入计数
|
||||
: type loop: int
|
||||
: param skip: 是否跳过
|
||||
: type skip: bool
|
||||
: param retry: 重试次数
|
||||
: type retry: int
|
||||
: param version: 版本号
|
||||
: type version: str
|
||||
: param inputs: 输入数据
|
||||
: type inputs: dict
|
||||
: param outputs: 输出数据
|
||||
: type outputs: dict
|
||||
"""
|
||||
self.id = id
|
||||
self.node_id = node_id
|
||||
self.started_time = started_time
|
||||
self.archived_time = archived_time
|
||||
self.loop = loop
|
||||
self.skip = skip
|
||||
self.retry = retry
|
||||
self.version = version
|
||||
self.inputs = inputs
|
||||
self.outputs = outputs
|
||||
|
||||
|
||||
class ExecutionShortHistory(Representable):
|
||||
"""
|
||||
简短节点执行历史
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
id: str,
|
||||
node_id: str,
|
||||
started_time: datetime,
|
||||
archived_time: datetime,
|
||||
loop: int,
|
||||
skip: bool,
|
||||
retry: int,
|
||||
version: str,
|
||||
):
|
||||
"""
|
||||
|
||||
: param id: ID
|
||||
: type id: str
|
||||
: param node_id: Node ID
|
||||
: type node_id: str
|
||||
: param started_time: 开始时间
|
||||
: type started_time: datetime
|
||||
: param archived_time: 归档时间
|
||||
: type archived_time: datetime
|
||||
: param loop: 重入计数
|
||||
: type loop: int
|
||||
: param skip: 是否跳过
|
||||
: type skip: bool
|
||||
: param retry: 重试次数
|
||||
: type retry: int
|
||||
: param version: 版本号
|
||||
: type version: str
|
||||
"""
|
||||
self.id = id
|
||||
self.node_id = node_id
|
||||
self.started_time = started_time
|
||||
self.archived_time = archived_time
|
||||
self.loop = loop
|
||||
self.skip = skip
|
||||
self.retry = retry
|
||||
self.version = version
|
||||
|
||||
|
||||
class CallbackData(Representable):
|
||||
"""
|
||||
节点回调数据
|
||||
"""
|
||||
|
||||
def __init__(self, id: int, node_id: str, version: str, data: dict):
|
||||
"""
|
||||
|
||||
:param id: 数据 ID
|
||||
:type id: int
|
||||
:param node_id: 节点 ID
|
||||
:type node_id: str
|
||||
:param version: 版本
|
||||
:type version: str
|
||||
:param data: 数据
|
||||
:type data: dict
|
||||
"""
|
||||
self.id = id
|
||||
self.node_id = node_id
|
||||
self.version = version
|
||||
self.data = data
|
||||
|
||||
|
||||
class SuspendedProcessInfo(Representable):
|
||||
"""
|
||||
挂起进程信息
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
process_id: int,
|
||||
current_node: str,
|
||||
root_pipeline_id: str,
|
||||
pipeline_stack: List[str],
|
||||
):
|
||||
"""
|
||||
|
||||
:param process_id: 进程 ID
|
||||
:type process_id: int
|
||||
:param current_node: 当前节点 ID
|
||||
:type current_node: str
|
||||
:param root_pipeline_id: 根流程 ID
|
||||
:type root_pipeline_id: str
|
||||
:param pipeline_stack: 流程栈
|
||||
:type pipeline_stack: List[str]
|
||||
"""
|
||||
self.process_id = process_id
|
||||
self.current_node = current_node
|
||||
self.root_pipeline_id = root_pipeline_id
|
||||
self.pipeline_stack = pipeline_stack
|
||||
|
||||
@property
|
||||
def top_pipeline_id(self):
|
||||
return self.pipeline_stack[-1]
|
||||
|
||||
|
||||
class ProcessInfo(Representable):
|
||||
"""
|
||||
进程信息
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
process_id: int,
|
||||
destination_id: str,
|
||||
root_pipeline_id: str,
|
||||
pipeline_stack: List[str],
|
||||
parent_id: int,
|
||||
):
|
||||
"""
|
||||
|
||||
:param process_id: 进程 ID
|
||||
:type process_id: int
|
||||
:param destination_id: 进程目标节点 ID
|
||||
:type destination_id: str
|
||||
:param root_pipeline_id: 根流程 ID
|
||||
:type root_pipeline_id: str
|
||||
:param pipeline_stack: 流程栈
|
||||
:type pipeline_stack: List[str]
|
||||
:param parent_id: 父进程 ID
|
||||
:type parent_id: int
|
||||
"""
|
||||
self.process_id = process_id
|
||||
self.destination_id = destination_id
|
||||
self.parent_id = parent_id
|
||||
self.root_pipeline_id = root_pipeline_id
|
||||
self.pipeline_stack = pipeline_stack
|
||||
|
||||
@property
|
||||
def top_pipeline_id(self):
|
||||
return self.pipeline_stack[-1]
|
||||
|
||||
|
||||
class DispatchProcess(Representable):
|
||||
"""
|
||||
待调度进程信息
|
||||
"""
|
||||
|
||||
def __init__(self, process_id: int, node_id: str):
|
||||
"""
|
||||
|
||||
:param process_id: 进程 ID
|
||||
:type process_id: int
|
||||
:param node_id: 调度开始节点 ID
|
||||
:type node_id: str
|
||||
"""
|
||||
self.process_id = process_id
|
||||
self.node_id = node_id
|
||||
|
||||
|
||||
class ContextValueType(Enum):
|
||||
"""
|
||||
|
||||
:param Enum: [description]
|
||||
:type Enum: [type]
|
||||
"""
|
||||
|
||||
PLAIN = 1
|
||||
SPLICE = 2
|
||||
COMPUTE = 3
|
||||
|
||||
|
||||
class ContextValue(Representable):
|
||||
def __init__(self, key: str, type: ContextValueType, value: Any, code: Optional[str] = None):
|
||||
if type is ContextValueType.COMPUTE and code is None:
|
||||
raise ValueError("code can't be none when type is COMPUTE")
|
||||
|
||||
self.key = key
|
||||
self.type = type
|
||||
self.value = value
|
||||
self.code = code
|
||||
61
lib/bamboo_engine/exceptions.py
Normal file
61
lib/bamboo_engine/exceptions.py
Normal file
@@ -0,0 +1,61 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS Community
|
||||
Edition) available.
|
||||
Copyright (C) 2017-2021 THL A29 Limited, a Tencent company. All rights reserved.
|
||||
Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
http://opensource.org/licenses/MIT
|
||||
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.
|
||||
"""
|
||||
|
||||
# 异常定义模块
|
||||
|
||||
|
||||
class EngineException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class InvalidOperationError(EngineException):
|
||||
pass
|
||||
|
||||
|
||||
class NotFoundError(EngineException):
|
||||
pass
|
||||
|
||||
|
||||
class ValueError(EngineException):
|
||||
pass
|
||||
|
||||
|
||||
class StateVersionNotMatchError(EngineException):
|
||||
pass
|
||||
|
||||
|
||||
class TreeInvalidException(EngineException):
|
||||
pass
|
||||
|
||||
|
||||
class ConnectionValidateError(TreeInvalidException):
|
||||
def __init__(self, failed_nodes, detail, *args):
|
||||
self.failed_nodes = failed_nodes
|
||||
self.detail = detail
|
||||
super(ConnectionValidateError, self).__init__(*args)
|
||||
|
||||
|
||||
class ConvergeMatchError(TreeInvalidException):
|
||||
def __init__(self, gateway_id, *args):
|
||||
self.gateway_id = gateway_id
|
||||
super(ConvergeMatchError, self).__init__(*args)
|
||||
|
||||
|
||||
class StreamValidateError(TreeInvalidException):
|
||||
def __init__(self, node_id, *args):
|
||||
self.node_id = node_id
|
||||
super(StreamValidateError, self).__init__(*args)
|
||||
|
||||
|
||||
class IsolateNodeError(TreeInvalidException):
|
||||
pass
|
||||
243
lib/bamboo_engine/handler.py
Normal file
243
lib/bamboo_engine/handler.py
Normal file
@@ -0,0 +1,243 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS Community
|
||||
Edition) available.
|
||||
Copyright (C) 2017-2021 THL A29 Limited, a Tencent company. All rights reserved.
|
||||
Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
http://opensource.org/licenses/MIT
|
||||
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.
|
||||
"""
|
||||
|
||||
# 节点处理器逻辑封装模块
|
||||
|
||||
from typing import Optional, List
|
||||
from abc import ABCMeta, abstractmethod
|
||||
|
||||
from bamboo_engine import states
|
||||
|
||||
from .eri import (
|
||||
EngineRuntimeInterface,
|
||||
Node,
|
||||
Schedule,
|
||||
CallbackData,
|
||||
ScheduleType,
|
||||
DispatchProcess,
|
||||
ProcessInfo,
|
||||
NodeType,
|
||||
)
|
||||
from .exceptions import NotFoundError, InvalidOperationError
|
||||
|
||||
|
||||
def register_handler(type: NodeType):
|
||||
"""
|
||||
节点 Handler 注册函数
|
||||
|
||||
:param type: 节点类型
|
||||
:type type: NodeType
|
||||
"""
|
||||
|
||||
def register(cls):
|
||||
HandlerFactory.add_handler(type, cls)
|
||||
return cls
|
||||
|
||||
return register
|
||||
|
||||
|
||||
class ExecuteResult:
|
||||
"""
|
||||
Handler execute 方法返回的结果
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
should_sleep: bool,
|
||||
schedule_ready: bool,
|
||||
schedule_type: Optional[ScheduleType],
|
||||
schedule_after: int,
|
||||
dispatch_processes: List[DispatchProcess],
|
||||
next_node_id: Optional[str],
|
||||
should_die: bool = False,
|
||||
):
|
||||
"""
|
||||
|
||||
:param should_sleep: 当前进程是否应该进入休眠
|
||||
:type should_sleep: bool
|
||||
:param schedule_ready: 被处理的节点是否准备好进入调度
|
||||
:type schedule_ready: bool
|
||||
:param schedule_type: 被处理的节点的调度类型
|
||||
:type schedule_type: Optional[ScheduleType]
|
||||
:param schedule_after: 在 schedule_after 秒后开始执行调度
|
||||
:type schedule_after: int
|
||||
:param dispatch_processes: 需要派发的子进程信息列表
|
||||
:type dispatch_processes: List[DispatchProcess]
|
||||
:param next_node_id: 推进循环中下一个要处理的节点的 ID
|
||||
:type next_node_id: Optional[str]
|
||||
:param should_die: 当前进程是否需要进入死亡状态, defaults to False
|
||||
:type should_die: bool, optional
|
||||
"""
|
||||
self.should_sleep = should_sleep
|
||||
self.schedule_ready = schedule_ready
|
||||
self.schedule_type = schedule_type
|
||||
self.schedule_after = schedule_after
|
||||
self.dispatch_processes = dispatch_processes
|
||||
self.next_node_id = next_node_id
|
||||
self.should_die = should_die
|
||||
|
||||
|
||||
class ScheduleResult:
|
||||
"""
|
||||
Handler schedule 方法返回的结果
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
has_next_schedule: bool,
|
||||
schedule_after: int,
|
||||
schedule_done: bool,
|
||||
next_node_id: Optional[str],
|
||||
):
|
||||
"""
|
||||
|
||||
:param has_next_schedule: 是否还有下次调度
|
||||
:type has_next_schedule: bool
|
||||
:param schedule_after: 在 schedule_after 秒后开始下次调度
|
||||
:type schedule_after: int
|
||||
:param schedule_done: 调度是否完成
|
||||
:type schedule_done: bool
|
||||
:param next_node_id: 调度完成后下一个需要执行的节点的 ID
|
||||
:type next_node_id: Optional[str]
|
||||
"""
|
||||
self.has_next_schedule = has_next_schedule
|
||||
self.schedule_after = schedule_after
|
||||
self.schedule_done = schedule_done
|
||||
self.next_node_id = next_node_id
|
||||
|
||||
|
||||
class NodeHandler(metaclass=ABCMeta):
|
||||
"""
|
||||
节点处理器,负责封装不同类型节点的 execute 和 schedule 逻辑
|
||||
"""
|
||||
|
||||
LOOP_KEY = "_loop"
|
||||
INNER_LOOP_KEY = "_inner_loop"
|
||||
|
||||
def __init__(self, node: Node, runtime: EngineRuntimeInterface):
|
||||
"""
|
||||
|
||||
:param node: 节点实例
|
||||
:type node: Node
|
||||
:param runtime: 引擎运行时实例
|
||||
:type runtime: EngineRuntimeInterface
|
||||
"""
|
||||
self.node = node
|
||||
self.runtime = runtime
|
||||
|
||||
@abstractmethod
|
||||
def execute(self, process_info: ProcessInfo, loop: int, inner_loop: int, version: str) -> ExecuteResult:
|
||||
"""
|
||||
节点的 execute 处理逻辑
|
||||
|
||||
:param process_info: 进程信息
|
||||
:type process_id: ProcessInfo
|
||||
:param loop: 重入次数
|
||||
:type loop: int
|
||||
:param inner_loop: 当前流程重入次数
|
||||
:type inner_loop: int
|
||||
:param version: 执行版本
|
||||
:type version: str
|
||||
:return: 执行结果
|
||||
:rtype: ExecuteResult
|
||||
"""
|
||||
|
||||
def schedule(
|
||||
self,
|
||||
process_info: ProcessInfo,
|
||||
loop: int,
|
||||
inner_loop: int,
|
||||
schedule: Schedule,
|
||||
callback_data: Optional[CallbackData] = None,
|
||||
) -> ScheduleResult:
|
||||
"""
|
||||
节点的 schedule 处理逻辑,不支持 schedule 的节点可以不实现该方法
|
||||
|
||||
:param process_info: 进程信息
|
||||
:type process_id: ProcessInfo
|
||||
:param loop: 重入次数
|
||||
:type loop: int
|
||||
:param inner_loop: 当前流程重入次数
|
||||
:type inner_loop: int
|
||||
:param schedule: Schedule 实例
|
||||
:type schedule: Schedule
|
||||
:param callback_data: 回调数据, defaults to None
|
||||
:type callback_data: Optional[CallbackData], optional
|
||||
:return: 调度结果
|
||||
:rtype: ScheduleResult
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def _execute_fail(self, ex_data: str) -> ExecuteResult:
|
||||
exec_outputs = self.runtime.get_execution_data_outputs(self.node.id)
|
||||
|
||||
self.runtime.set_state(node_id=self.node.id, to_state=states.FAILED, set_archive_time=True)
|
||||
|
||||
exec_outputs["ex_data"] = ex_data
|
||||
|
||||
self.runtime.set_execution_data_outputs(self.node.id, exec_outputs)
|
||||
|
||||
return ExecuteResult(
|
||||
should_sleep=True,
|
||||
schedule_ready=False,
|
||||
schedule_type=None,
|
||||
schedule_after=-1,
|
||||
dispatch_processes=[],
|
||||
next_node_id=None,
|
||||
)
|
||||
|
||||
def _get_plain_inputs(self, node_id: str):
|
||||
return {key: di.value for key, di in self.runtime.get_data_inputs(node_id).items()}
|
||||
|
||||
|
||||
class HandlerFactory:
|
||||
"""
|
||||
节点处理器工厂
|
||||
"""
|
||||
|
||||
_handlers = {}
|
||||
|
||||
@classmethod
|
||||
def add_handler(cls, type: NodeType, handler_cls):
|
||||
"""
|
||||
向工厂中注册某个类型节点的处理器
|
||||
|
||||
:param type: 节点类型
|
||||
:type type: NodeType
|
||||
:param handler_cls: [description]
|
||||
:type handler_cls: [type]
|
||||
:raises InvalidOperationError: [description]
|
||||
"""
|
||||
if not issubclass(handler_cls, NodeHandler):
|
||||
raise InvalidOperationError(
|
||||
"register handler err: {} is not subclass of {}".format(handler_cls, "NodeHandler")
|
||||
)
|
||||
cls._handlers[type.value] = handler_cls
|
||||
|
||||
@classmethod
|
||||
def get_handler(cls, node: Node, runtime: EngineRuntimeInterface) -> NodeHandler:
|
||||
"""
|
||||
获取某个节点的处理器实例
|
||||
|
||||
:param node: 节点实例
|
||||
:type node: NodeType
|
||||
:param runtime: 引擎运行时实例
|
||||
:type runtime: EngineRuntimeInterface
|
||||
:raises NotFoundError: [description]
|
||||
:return: 节点处理器实例
|
||||
:rtype: NodeHandler
|
||||
"""
|
||||
if node.type.value not in cls._handlers:
|
||||
raise NotFoundError("can not find handler for {} type node".format(node.type.value))
|
||||
|
||||
return cls._handlers[node.type.value](node, runtime)
|
||||
28
lib/bamboo_engine/handlers/__init__.py
Normal file
28
lib/bamboo_engine/handlers/__init__.py
Normal file
@@ -0,0 +1,28 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS Community
|
||||
Edition) available.
|
||||
Copyright (C) 2017-2021 THL A29 Limited, a Tencent company. All rights reserved.
|
||||
Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
http://opensource.org/licenses/MIT
|
||||
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.
|
||||
"""
|
||||
|
||||
"""
|
||||
节点处理逻辑存放模块
|
||||
"""
|
||||
|
||||
|
||||
def register():
|
||||
from .conditional_parallel_gateway import ConditionalParallelGatewayHandler # noqa
|
||||
from .converge_gateway import ConvergeGatewayHandler # noqa
|
||||
from .empty_end_event import EmptyEndEventHandler # noqa
|
||||
from .empty_start_event import EmptyStartEventHandler # noqa
|
||||
from .exclusive_gateway import ExclusiveGatewayHandler # noqa
|
||||
from .executable_end_event import ExecutableEndEventHandler # noqa
|
||||
from .parallel_gateway import ParallelGatewayHandler # noqa
|
||||
from .service_activity import ServiceActivityHandler # noqa
|
||||
from .subprocess import SubProcessHandler # noqa
|
||||
137
lib/bamboo_engine/handlers/conditional_parallel_gateway.py
Normal file
137
lib/bamboo_engine/handlers/conditional_parallel_gateway.py
Normal file
@@ -0,0 +1,137 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS Community
|
||||
Edition) available.
|
||||
Copyright (C) 2017-2021 THL A29 Limited, a Tencent company. All rights reserved.
|
||||
Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
http://opensource.org/licenses/MIT
|
||||
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 json
|
||||
import logging
|
||||
|
||||
from bamboo_engine.utils.boolrule import BoolRule
|
||||
from bamboo_engine.template.template import Template
|
||||
|
||||
from bamboo_engine import states
|
||||
from bamboo_engine.eri import NodeType, ProcessInfo
|
||||
from bamboo_engine.context import Context
|
||||
from bamboo_engine.handler import register_handler, NodeHandler, ExecuteResult
|
||||
from bamboo_engine.utils.string import transform_escape_char
|
||||
|
||||
logger = logging.getLogger("bamboo_engine")
|
||||
|
||||
|
||||
@register_handler(NodeType.ConditionalParallelGateway)
|
||||
class ConditionalParallelGatewayHandler(NodeHandler):
|
||||
def execute(self, process_info: ProcessInfo, loop: int, inner_loop: int, version: str) -> ExecuteResult:
|
||||
"""
|
||||
节点的 execute 处理逻辑
|
||||
|
||||
:param runtime: 引擎运行时实例
|
||||
:type runtime: EngineRuntimeInterface
|
||||
:param process_info: 进程信息
|
||||
:type process_id: ProcessInfo
|
||||
:return: 执行结果
|
||||
:rtype: ExecuteResult
|
||||
"""
|
||||
evaluations = [c.evaluation for c in self.node.conditions]
|
||||
top_pipeline_id = process_info.top_pipeline_id
|
||||
root_pipeline_id = process_info.root_pipeline_id
|
||||
|
||||
root_pipeline_inputs = self._get_plain_inputs(root_pipeline_id)
|
||||
|
||||
# resolve conditions references
|
||||
evaluation_refs = set()
|
||||
for e in evaluations:
|
||||
refs = Template(e).get_reference()
|
||||
evaluation_refs = evaluation_refs.union(refs)
|
||||
|
||||
logger.info(
|
||||
"root_pipeline[%s] node(%s) evaluation original refs: %s",
|
||||
root_pipeline_id,
|
||||
self.node.id,
|
||||
evaluation_refs,
|
||||
)
|
||||
additional_refs = self.runtime.get_context_key_references(pipeline_id=top_pipeline_id, keys=evaluation_refs)
|
||||
evaluation_refs = evaluation_refs.union(additional_refs)
|
||||
|
||||
logger.info(
|
||||
"root_pipeline[%s] node(%s) evaluation final refs: %s",
|
||||
root_pipeline_id,
|
||||
self.node.id,
|
||||
evaluation_refs,
|
||||
)
|
||||
context_values = self.runtime.get_context_values(pipeline_id=top_pipeline_id, keys=evaluation_refs)
|
||||
context = Context(self.runtime, context_values, root_pipeline_inputs)
|
||||
try:
|
||||
hydrated_context = {k: transform_escape_char(v) for k, v in context.hydrate(deformat=True).items()}
|
||||
except Exception as e:
|
||||
logger.exception(
|
||||
"root_pipeline[%s] node(%s) context hydrate error",
|
||||
root_pipeline_id,
|
||||
self.node.id,
|
||||
)
|
||||
return self._execute_fail("evaluation context hydrate failed(%s), check node log for details." % e)
|
||||
|
||||
# check conditions
|
||||
fork_targets = []
|
||||
for c in self.node.conditions:
|
||||
resolved_evaluate = Template(c.evaluation).render(hydrated_context)
|
||||
logger.info(
|
||||
"root_pipeline[%s] node(%s) render evaluation %s: %s with %s",
|
||||
root_pipeline_id,
|
||||
self.node.id,
|
||||
c.evaluation,
|
||||
resolved_evaluate,
|
||||
hydrated_context,
|
||||
)
|
||||
try:
|
||||
result = BoolRule(resolved_evaluate).test()
|
||||
logger.info(
|
||||
"root_pipeline[%s] node(%s) %s test result: %s",
|
||||
root_pipeline_id,
|
||||
self.node.id,
|
||||
resolved_evaluate,
|
||||
result,
|
||||
)
|
||||
except Exception as e:
|
||||
# test failed
|
||||
return self._execute_fail(
|
||||
"evaluate[{}] fail with data[{}] message: {}".format(
|
||||
c.resolved_evaluate, json.dumps(hydrated_context), e
|
||||
)
|
||||
)
|
||||
else:
|
||||
if result:
|
||||
fork_targets.append(c.target_id)
|
||||
|
||||
# all miss
|
||||
if not fork_targets:
|
||||
return self._execute_fail("all conditions of branches are not meet")
|
||||
|
||||
# fork
|
||||
from_to = {}
|
||||
for target in fork_targets:
|
||||
from_to[target] = self.node.converge_gateway_id
|
||||
|
||||
dispatch_processes = self.runtime.fork(
|
||||
parent_id=process_info.process_id,
|
||||
root_pipeline_id=process_info.root_pipeline_id,
|
||||
pipeline_stack=process_info.pipeline_stack,
|
||||
from_to=from_to,
|
||||
)
|
||||
|
||||
self.runtime.set_state(node_id=self.node.id, to_state=states.FINISHED, set_archive_time=True)
|
||||
|
||||
return ExecuteResult(
|
||||
should_sleep=True,
|
||||
schedule_ready=False,
|
||||
schedule_type=None,
|
||||
schedule_after=-1,
|
||||
dispatch_processes=dispatch_processes,
|
||||
next_node_id=None,
|
||||
)
|
||||
42
lib/bamboo_engine/handlers/converge_gateway.py
Normal file
42
lib/bamboo_engine/handlers/converge_gateway.py
Normal file
@@ -0,0 +1,42 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS Community
|
||||
Edition) available.
|
||||
Copyright (C) 2017-2021 THL A29 Limited, a Tencent company. All rights reserved.
|
||||
Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
http://opensource.org/licenses/MIT
|
||||
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.
|
||||
"""
|
||||
|
||||
from bamboo_engine import states
|
||||
from bamboo_engine.eri import ProcessInfo, NodeType
|
||||
from bamboo_engine.handler import register_handler, NodeHandler, ExecuteResult
|
||||
|
||||
|
||||
@register_handler(NodeType.ConvergeGateway)
|
||||
class ConvergeGatewayHandler(NodeHandler):
|
||||
def execute(self, process_info: ProcessInfo, loop: int, inner_loop: int, version: str) -> ExecuteResult:
|
||||
"""
|
||||
节点的 execute 处理逻辑
|
||||
|
||||
:param runtime: 引擎运行时实例
|
||||
:type runtime: EngineRuntimeInterface
|
||||
:param process_info: 进程信息
|
||||
:type process_id: ProcessInfo
|
||||
:return: 执行结果
|
||||
:rtype: ExecuteResult
|
||||
"""
|
||||
|
||||
self.runtime.set_state(node_id=self.node.id, to_state=states.FINISHED, set_archive_time=True)
|
||||
|
||||
return ExecuteResult(
|
||||
should_sleep=False,
|
||||
schedule_ready=False,
|
||||
schedule_type=None,
|
||||
schedule_after=-1,
|
||||
dispatch_processes=[],
|
||||
next_node_id=self.node.target_nodes[0],
|
||||
)
|
||||
135
lib/bamboo_engine/handlers/empty_end_event.py
Normal file
135
lib/bamboo_engine/handlers/empty_end_event.py
Normal file
@@ -0,0 +1,135 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS Community
|
||||
Edition) available.
|
||||
Copyright (C) 2017-2021 THL A29 Limited, a Tencent company. All rights reserved.
|
||||
Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
http://opensource.org/licenses/MIT
|
||||
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 logging
|
||||
|
||||
from bamboo_engine import states
|
||||
from bamboo_engine.config import Settings
|
||||
from bamboo_engine.eri import ProcessInfo, NodeType
|
||||
from bamboo_engine.handler import register_handler, NodeHandler, ExecuteResult
|
||||
from bamboo_engine.context import Context
|
||||
from bamboo_engine.template.template import Template
|
||||
|
||||
logger = logging.getLogger("bamboo_engine")
|
||||
|
||||
|
||||
@register_handler(NodeType.EmptyEndEvent)
|
||||
class EmptyEndEventHandler(NodeHandler):
|
||||
def execute(self, process_info: ProcessInfo, loop: int, inner_loop: int, version: str) -> ExecuteResult:
|
||||
"""
|
||||
节点的 execute 处理逻辑
|
||||
|
||||
:param runtime: 引擎运行时实例
|
||||
:type runtime: EngineRuntimeInterface
|
||||
:param process_info: 进程信息
|
||||
:type process_id: ProcessInfo
|
||||
:return: 执行结果
|
||||
:rtype: ExecuteResult
|
||||
"""
|
||||
root_pipeline_id = process_info.root_pipeline_id
|
||||
pipeline_id = process_info.pipeline_stack.pop()
|
||||
root_pipeline_finished = len(process_info.pipeline_stack) == 0
|
||||
|
||||
root_pipeline_inputs = self._get_plain_inputs(process_info.root_pipeline_id)
|
||||
if not root_pipeline_finished:
|
||||
subproc_state = self.runtime.get_state(pipeline_id)
|
||||
|
||||
# write pipeline data
|
||||
context_outputs = self.runtime.get_context_outputs(pipeline_id)
|
||||
logger.info(
|
||||
"root_pipeline[%s] pipeline(%s) context outputs: %s",
|
||||
root_pipeline_id,
|
||||
pipeline_id,
|
||||
context_outputs,
|
||||
)
|
||||
|
||||
context_values = self.runtime.get_context_values(pipeline_id=pipeline_id, keys=context_outputs)
|
||||
logger.info(
|
||||
"root_pipeline[%s] pipeline(%s) context values: %s",
|
||||
root_pipeline_id,
|
||||
pipeline_id,
|
||||
context_values,
|
||||
)
|
||||
|
||||
# caculate outputs values references
|
||||
output_value_refs = set(Template([cv.value for cv in context_values]).get_reference())
|
||||
logger.info(
|
||||
"root_pipeline[%s] node(%s) outputs values refs: %s",
|
||||
root_pipeline_id,
|
||||
self.node.id,
|
||||
output_value_refs,
|
||||
)
|
||||
|
||||
additional_refs = self.runtime.get_context_key_references(pipeline_id=pipeline_id, keys=output_value_refs)
|
||||
output_value_refs = output_value_refs.union(additional_refs)
|
||||
logger.info(
|
||||
"root_pipeline[%s] pipeline(%s) outputs values final refs: %s",
|
||||
root_pipeline_id,
|
||||
pipeline_id,
|
||||
output_value_refs,
|
||||
)
|
||||
context_values.extend(self.runtime.get_context_values(pipeline_id=pipeline_id, keys=output_value_refs))
|
||||
|
||||
context = Context(self.runtime, context_values, root_pipeline_inputs)
|
||||
hydrated_context = context.hydrate(deformat=False)
|
||||
logger.info(
|
||||
"root_pipeline[%s] pipeline(%s) hydrated context: %s",
|
||||
root_pipeline_id,
|
||||
pipeline_id,
|
||||
hydrated_context,
|
||||
)
|
||||
|
||||
outputs = {}
|
||||
for key in context_outputs:
|
||||
outputs[key] = hydrated_context.get(key, key)
|
||||
if not root_pipeline_finished:
|
||||
outputs[self.LOOP_KEY] = subproc_state.loop + Settings.RERUN_INDEX_OFFSET
|
||||
outputs[self.INNER_LOOP_KEY] = subproc_state.inner_loop + Settings.RERUN_INDEX_OFFSET
|
||||
self.runtime.set_execution_data_outputs(node_id=pipeline_id, outputs=outputs)
|
||||
|
||||
self.runtime.set_state(node_id=self.node.id, to_state=states.FINISHED, set_archive_time=True)
|
||||
|
||||
self.runtime.set_state(node_id=pipeline_id, to_state=states.FINISHED, set_archive_time=True)
|
||||
|
||||
# root pipeline finish
|
||||
if root_pipeline_finished:
|
||||
return ExecuteResult(
|
||||
should_sleep=False,
|
||||
schedule_ready=False,
|
||||
schedule_type=None,
|
||||
schedule_after=-1,
|
||||
dispatch_processes=[],
|
||||
next_node_id=None,
|
||||
should_die=True,
|
||||
)
|
||||
|
||||
# subprocess finish
|
||||
subprocess = self.runtime.get_node(pipeline_id)
|
||||
self.runtime.set_pipeline_stack(process_info.process_id, process_info.pipeline_stack)
|
||||
|
||||
# extract subprocess outputs to parent context
|
||||
subprocess_outputs = self.runtime.get_data_outputs(pipeline_id)
|
||||
context.extract_outputs(
|
||||
pipeline_id=process_info.pipeline_stack[-1],
|
||||
data_outputs=subprocess_outputs,
|
||||
execution_data_outputs=outputs,
|
||||
)
|
||||
|
||||
return ExecuteResult(
|
||||
should_sleep=False,
|
||||
schedule_ready=False,
|
||||
schedule_type=None,
|
||||
schedule_after=-1,
|
||||
dispatch_processes=[],
|
||||
next_node_id=subprocess.target_nodes[0],
|
||||
)
|
||||
81
lib/bamboo_engine/handlers/empty_start_event.py
Normal file
81
lib/bamboo_engine/handlers/empty_start_event.py
Normal file
@@ -0,0 +1,81 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS Community
|
||||
Edition) available.
|
||||
Copyright (C) 2017-2021 THL A29 Limited, a Tencent company. All rights reserved.
|
||||
Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
http://opensource.org/licenses/MIT
|
||||
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 logging
|
||||
|
||||
from bamboo_engine import states
|
||||
from bamboo_engine.context import Context
|
||||
from bamboo_engine.eri import ProcessInfo, NodeType, ContextValue, ContextValueType
|
||||
from bamboo_engine.exceptions import NotFoundError
|
||||
from bamboo_engine.handler import register_handler, NodeHandler, ExecuteResult
|
||||
|
||||
logger = logging.getLogger("bamboo_engine")
|
||||
|
||||
|
||||
@register_handler(NodeType.EmptyStartEvent)
|
||||
class EmptyStartEventHandler(NodeHandler):
|
||||
def execute(self, process_info: ProcessInfo, loop: int, inner_loop: int, version: str) -> ExecuteResult:
|
||||
"""
|
||||
节点的 execute 处理逻辑
|
||||
|
||||
:param runtime: 引擎运行时实例
|
||||
:type runtime: EngineRuntimeInterface
|
||||
:param process_info: 进程信息
|
||||
:type process_id: ProcessInfo
|
||||
:return: 执行结果
|
||||
:rtype: ExecuteResult
|
||||
"""
|
||||
|
||||
try:
|
||||
data = self.runtime.get_data(self.node.id)
|
||||
except NotFoundError:
|
||||
need_pre_render = False
|
||||
else:
|
||||
need_pre_render = True
|
||||
|
||||
if need_pre_render:
|
||||
top_pipeline_id = process_info.top_pipeline_id
|
||||
root_pipeline_inputs = self._get_plain_inputs(process_info.root_pipeline_id)
|
||||
upsert_context_dict = dict()
|
||||
pre_render_keys = data.inputs["pre_render_keys"].value
|
||||
|
||||
logger.info("top_pipeline({}) pre_render_keys are: {}".format(top_pipeline_id, ",".join(pre_render_keys)))
|
||||
|
||||
refs = self.runtime.get_context_key_references(pipeline_id=top_pipeline_id, keys=set(pre_render_keys))
|
||||
|
||||
context_values = self.runtime.get_context_values(
|
||||
pipeline_id=top_pipeline_id, keys=set(pre_render_keys).union(refs)
|
||||
)
|
||||
context = Context(self.runtime, context_values, root_pipeline_inputs)
|
||||
hydrated_context = context.hydrate(deformat=False)
|
||||
for context_value in context_values:
|
||||
context_key = context_value.key
|
||||
if context_key in pre_render_keys:
|
||||
upsert_context_dict[context_key] = ContextValue(
|
||||
key=context_key,
|
||||
type=ContextValueType.PLAIN,
|
||||
value=hydrated_context[context_key],
|
||||
)
|
||||
|
||||
logger.info(f"top_pipeline({top_pipeline_id}) pre_render_keys results are: {upsert_context_dict}")
|
||||
self.runtime.upsert_plain_context_values(top_pipeline_id, upsert_context_dict)
|
||||
|
||||
self.runtime.set_state(node_id=self.node.id, to_state=states.FINISHED, set_archive_time=True)
|
||||
|
||||
return ExecuteResult(
|
||||
should_sleep=False,
|
||||
schedule_ready=False,
|
||||
schedule_type=None,
|
||||
schedule_after=-1,
|
||||
dispatch_processes=[],
|
||||
next_node_id=self.node.target_nodes[0],
|
||||
)
|
||||
138
lib/bamboo_engine/handlers/exclusive_gateway.py
Normal file
138
lib/bamboo_engine/handlers/exclusive_gateway.py
Normal file
@@ -0,0 +1,138 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS Community
|
||||
Edition) available.
|
||||
Copyright (C) 2017-2021 THL A29 Limited, a Tencent company. All rights reserved.
|
||||
Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
http://opensource.org/licenses/MIT
|
||||
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 json
|
||||
import logging
|
||||
|
||||
from bamboo_engine import states
|
||||
from bamboo_engine.context import Context
|
||||
from bamboo_engine.template import Template
|
||||
from bamboo_engine.handler import register_handler, NodeHandler, ExecuteResult
|
||||
from bamboo_engine.utils.boolrule import BoolRule
|
||||
from bamboo_engine.eri import NodeType, ProcessInfo
|
||||
|
||||
from bamboo_engine.utils.string import transform_escape_char
|
||||
|
||||
logger = logging.getLogger("bamboo_engine")
|
||||
|
||||
|
||||
@register_handler(NodeType.ExclusiveGateway)
|
||||
class ExclusiveGatewayHandler(NodeHandler):
|
||||
def execute(self, process_info: ProcessInfo, loop: int, inner_loop: int, version: str) -> ExecuteResult:
|
||||
"""
|
||||
节点的 execute 处理逻辑
|
||||
|
||||
:param runtime: 引擎运行时实例
|
||||
:type runtime: EngineRuntimeInterface
|
||||
:param process_info: 进程信息
|
||||
:type process_id: ProcessInfo
|
||||
:return: 执行结果
|
||||
:rtype: ExecuteResult
|
||||
"""
|
||||
evaluations = [c.evaluation for c in self.node.conditions]
|
||||
top_pipeline_id = process_info.top_pipeline_id
|
||||
root_pipeline_id = process_info.root_pipeline_id
|
||||
|
||||
root_pipeline_inputs = self._get_plain_inputs(process_info.root_pipeline_id)
|
||||
|
||||
# resolve conditions references
|
||||
evaluation_refs = set()
|
||||
for e in evaluations:
|
||||
refs = Template(e).get_reference()
|
||||
evaluation_refs = evaluation_refs.union(refs)
|
||||
|
||||
logger.info(
|
||||
"root_pipeline[%s] node(%s) evaluation original refs: %s",
|
||||
root_pipeline_id,
|
||||
self.node.id,
|
||||
evaluation_refs,
|
||||
)
|
||||
additional_refs = self.runtime.get_context_key_references(pipeline_id=top_pipeline_id, keys=evaluation_refs)
|
||||
evaluation_refs = evaluation_refs.union(additional_refs)
|
||||
|
||||
logger.info(
|
||||
"root_pipeline[%s] node(%s) evaluation final refs: %s",
|
||||
root_pipeline_id,
|
||||
self.node.id,
|
||||
evaluation_refs,
|
||||
)
|
||||
context_values = self.runtime.get_context_values(pipeline_id=top_pipeline_id, keys=evaluation_refs)
|
||||
logger.info(
|
||||
"root_pipeline[%s] node(%s) evaluation context values: %s",
|
||||
root_pipeline_id,
|
||||
self.node.id,
|
||||
context_values,
|
||||
)
|
||||
|
||||
context = Context(self.runtime, context_values, root_pipeline_inputs)
|
||||
try:
|
||||
hydrated_context = {k: transform_escape_char(v) for k, v in context.hydrate(deformat=True).items()}
|
||||
except Exception as e:
|
||||
logger.exception(
|
||||
"root_pipeline[%s] node(%s) context hydrate error",
|
||||
root_pipeline_id,
|
||||
self.node.id,
|
||||
)
|
||||
return self._execute_fail("evaluation context hydrate failed(%s), check node log for details." % e)
|
||||
|
||||
# check conditions
|
||||
meet_targets = []
|
||||
meet_conditions = []
|
||||
for c in self.node.conditions:
|
||||
resolved_evaluate = Template(c.evaluation).render(hydrated_context)
|
||||
logger.info(
|
||||
"root_pipeline[%s] node(%s) render evaluation %s: %s with %s",
|
||||
root_pipeline_id,
|
||||
self.node.id,
|
||||
c.evaluation,
|
||||
resolved_evaluate,
|
||||
hydrated_context,
|
||||
)
|
||||
try:
|
||||
result = BoolRule(resolved_evaluate).test()
|
||||
logger.info(
|
||||
"root_pipeline[%s] node(%s) %s test result: %s",
|
||||
root_pipeline_id,
|
||||
self.node.id,
|
||||
resolved_evaluate,
|
||||
result,
|
||||
)
|
||||
except Exception as e:
|
||||
# test failed
|
||||
return self._execute_fail(
|
||||
"evaluate[{}] fail with data[{}] message: {}".format(
|
||||
resolved_evaluate, json.dumps(hydrated_context), e
|
||||
)
|
||||
)
|
||||
else:
|
||||
if result:
|
||||
meet_conditions.append(c.name)
|
||||
meet_targets.append(c.target_id)
|
||||
|
||||
# all miss
|
||||
if not meet_targets:
|
||||
return self._execute_fail("all conditions of branches are not meet")
|
||||
|
||||
# multiple branch hit
|
||||
if len(meet_targets) != 1:
|
||||
return self._execute_fail("multiple conditions meet: {}".format(meet_conditions))
|
||||
|
||||
self.runtime.set_state(node_id=self.node.id, to_state=states.FINISHED, set_archive_time=True)
|
||||
|
||||
return ExecuteResult(
|
||||
should_sleep=False,
|
||||
schedule_ready=False,
|
||||
schedule_type=None,
|
||||
schedule_after=-1,
|
||||
dispatch_processes=[],
|
||||
next_node_id=meet_targets[0],
|
||||
)
|
||||
75
lib/bamboo_engine/handlers/executable_end_event.py
Normal file
75
lib/bamboo_engine/handlers/executable_end_event.py
Normal file
@@ -0,0 +1,75 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS Community
|
||||
Edition) available.
|
||||
Copyright (C) 2017-2021 THL A29 Limited, a Tencent company. All rights reserved.
|
||||
Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
http://opensource.org/licenses/MIT
|
||||
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 copy
|
||||
import logging
|
||||
import traceback
|
||||
|
||||
from bamboo_engine import states
|
||||
from bamboo_engine.eri import ProcessInfo, NodeType
|
||||
from bamboo_engine.handler import register_handler, ExecuteResult
|
||||
|
||||
from .empty_end_event import EmptyEndEventHandler
|
||||
|
||||
logger = logging.getLogger("bamboo_engine")
|
||||
|
||||
|
||||
@register_handler(NodeType.ExecutableEndEvent)
|
||||
class ExecutableEndEventHandler(EmptyEndEventHandler):
|
||||
def execute(self, process_info: ProcessInfo, loop: int, inner_loop: int, version: str) -> ExecuteResult:
|
||||
"""
|
||||
节点的 execute 处理逻辑
|
||||
|
||||
:param runtime: 引擎运行时实例
|
||||
:type runtime: EngineRuntimeInterface
|
||||
:param process_info: 进程信息
|
||||
:type process_id: ProcessInfo
|
||||
:return: 执行结果
|
||||
:rtype: ExecuteResult
|
||||
"""
|
||||
|
||||
logger.info(
|
||||
"root_pipeline[%s] node(%s) executable end event: %s",
|
||||
process_info.root_pipeline_id,
|
||||
self.node.id,
|
||||
self.node,
|
||||
)
|
||||
event = self.runtime.get_executable_end_event(code=self.node.code)
|
||||
|
||||
try:
|
||||
event.execute(
|
||||
pipeline_stack=copy.copy(process_info.pipeline_stack),
|
||||
root_pipeline_id=process_info.root_pipeline_id,
|
||||
)
|
||||
except Exception:
|
||||
ex_data = traceback.format_exc()
|
||||
logger.warning(
|
||||
"root_pipeline[%s] node(%s) executable end event execute raise: %s",
|
||||
process_info.root_pipeline_id,
|
||||
self.node.id,
|
||||
ex_data,
|
||||
)
|
||||
|
||||
self.runtime.set_execution_data_outputs(node_id=self.node.id, outputs={"ex_data": ex_data})
|
||||
|
||||
self.runtime.set_state(node_id=self.node.id, to_state=states.FAILED, set_archive_time=True)
|
||||
|
||||
return ExecuteResult(
|
||||
should_sleep=True,
|
||||
schedule_ready=False,
|
||||
schedule_type=None,
|
||||
schedule_after=-1,
|
||||
dispatch_processes=[],
|
||||
next_node_id=None,
|
||||
)
|
||||
|
||||
return super().execute(process_info=process_info, loop=loop, inner_loop=inner_loop, version=version)
|
||||
53
lib/bamboo_engine/handlers/parallel_gateway.py
Normal file
53
lib/bamboo_engine/handlers/parallel_gateway.py
Normal file
@@ -0,0 +1,53 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS Community
|
||||
Edition) available.
|
||||
Copyright (C) 2017-2021 THL A29 Limited, a Tencent company. All rights reserved.
|
||||
Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
http://opensource.org/licenses/MIT
|
||||
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.
|
||||
"""
|
||||
|
||||
from bamboo_engine import states
|
||||
from bamboo_engine.eri import ProcessInfo, NodeType
|
||||
from bamboo_engine.handler import register_handler, NodeHandler, ExecuteResult
|
||||
|
||||
|
||||
@register_handler(NodeType.ParallelGateway)
|
||||
class ParallelGatewayHandler(NodeHandler):
|
||||
def execute(self, process_info: ProcessInfo, loop: int, inner_loop: int, version: str) -> ExecuteResult:
|
||||
"""
|
||||
节点的 execute 处理逻辑
|
||||
|
||||
:param runtime: 引擎运行时实例
|
||||
:type runtime: EngineRuntimeInterface
|
||||
:param process_info: 进程信息
|
||||
:type process_id: ProcessInfo
|
||||
:return: 执行结果
|
||||
:rtype: ExecuteResult
|
||||
"""
|
||||
|
||||
from_to = {}
|
||||
for target in self.node.target_nodes:
|
||||
from_to[target] = self.node.converge_gateway_id
|
||||
|
||||
dispatch_processes = self.runtime.fork(
|
||||
parent_id=process_info.process_id,
|
||||
root_pipeline_id=process_info.root_pipeline_id,
|
||||
pipeline_stack=process_info.pipeline_stack,
|
||||
from_to=from_to,
|
||||
)
|
||||
|
||||
self.runtime.set_state(node_id=self.node.id, to_state=states.FINISHED, set_archive_time=True)
|
||||
|
||||
return ExecuteResult(
|
||||
should_sleep=True,
|
||||
schedule_ready=False,
|
||||
schedule_type=None,
|
||||
schedule_after=-1,
|
||||
dispatch_processes=dispatch_processes,
|
||||
next_node_id=None,
|
||||
)
|
||||
508
lib/bamboo_engine/handlers/service_activity.py
Normal file
508
lib/bamboo_engine/handlers/service_activity.py
Normal file
@@ -0,0 +1,508 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS Community
|
||||
Edition) available.
|
||||
Copyright (C) 2017-2021 THL A29 Limited, a Tencent company. All rights reserved.
|
||||
Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
http://opensource.org/licenses/MIT
|
||||
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 logging
|
||||
import traceback
|
||||
from typing import Optional
|
||||
|
||||
from bamboo_engine import states
|
||||
from bamboo_engine.config import Settings
|
||||
|
||||
from bamboo_engine.context import Context
|
||||
from bamboo_engine.template import Template
|
||||
from bamboo_engine.eri import (
|
||||
ProcessInfo,
|
||||
ContextValue,
|
||||
ContextValueType,
|
||||
ExecutionData,
|
||||
CallbackData,
|
||||
ScheduleType,
|
||||
NodeType,
|
||||
Schedule,
|
||||
)
|
||||
from bamboo_engine.handler import (
|
||||
register_handler,
|
||||
NodeHandler,
|
||||
ExecuteResult,
|
||||
ScheduleResult,
|
||||
)
|
||||
|
||||
logger = logging.getLogger("bamboo_engine")
|
||||
|
||||
|
||||
@register_handler(NodeType.ServiceActivity)
|
||||
class ServiceActivityHandler(NodeHandler):
|
||||
"""
|
||||
其中所有 set_state 调用都会传入 state version 来确保能够在用户强制失败节点后放弃后续无效的任务执行
|
||||
"""
|
||||
|
||||
def execute(self, process_info: ProcessInfo, loop: int, inner_loop: int, version: str) -> ExecuteResult:
|
||||
"""
|
||||
节点的 execute 处理逻辑
|
||||
|
||||
:param runtime: 引擎运行时实例
|
||||
:type runtime: EngineRuntimeInterface
|
||||
:param process_info: 进程信息
|
||||
:type process_id: ProcessInfo
|
||||
:return: 执行结果
|
||||
:rtype: ExecuteResult
|
||||
"""
|
||||
top_pipeline_id = process_info.top_pipeline_id
|
||||
root_pipeline_id = process_info.root_pipeline_id
|
||||
|
||||
data = self.runtime.get_data(self.node.id)
|
||||
root_pipeline_inputs = self._get_plain_inputs(process_info.root_pipeline_id)
|
||||
need_render_inputs = data.need_render_inputs()
|
||||
render_escape_inputs = data.render_escape_inputs()
|
||||
|
||||
logger.info(
|
||||
"root_pipeline[%s] node(%s) activity execute data: %s, root inputs: %s",
|
||||
root_pipeline_id,
|
||||
self.node.id,
|
||||
data,
|
||||
root_pipeline_inputs,
|
||||
)
|
||||
|
||||
# resolve inputs context references
|
||||
inputs_refs = set(Template(need_render_inputs).get_reference())
|
||||
logger.info(
|
||||
"root_pipeline[%s] node(%s) activity original refs: %s",
|
||||
root_pipeline_id,
|
||||
self.node.id,
|
||||
inputs_refs,
|
||||
)
|
||||
|
||||
additional_refs = self.runtime.get_context_key_references(pipeline_id=top_pipeline_id, keys=inputs_refs)
|
||||
inputs_refs = inputs_refs.union(additional_refs)
|
||||
logger.info(
|
||||
"root_pipeline[%s] node(%s) activity final refs: %s",
|
||||
root_pipeline_id,
|
||||
self.node.id,
|
||||
inputs_refs,
|
||||
)
|
||||
|
||||
# prepare context
|
||||
context_values = self.runtime.get_context_values(pipeline_id=top_pipeline_id, keys=inputs_refs)
|
||||
|
||||
# pre extract loop outputs
|
||||
loop_value = loop + Settings.RERUN_INDEX_OFFSET
|
||||
need_render_inputs[self.LOOP_KEY] = loop_value
|
||||
if self.LOOP_KEY in data.outputs:
|
||||
loop_output_key = data.outputs[self.LOOP_KEY]
|
||||
context_values.append(ContextValue(key=loop_output_key, type=ContextValueType.PLAIN, value=loop_value))
|
||||
|
||||
# pre extract inner_loop outputs
|
||||
inner_loop_value = inner_loop + Settings.RERUN_INDEX_OFFSET
|
||||
need_render_inputs[self.INNER_LOOP_KEY] = inner_loop_value
|
||||
if self.INNER_LOOP_KEY in data.outputs:
|
||||
inner_loop_output_key = data.outputs[self.INNER_LOOP_KEY]
|
||||
context_values.append(
|
||||
ContextValue(
|
||||
key=inner_loop_output_key,
|
||||
type=ContextValueType.PLAIN,
|
||||
value=inner_loop_value,
|
||||
)
|
||||
)
|
||||
|
||||
logger.info(
|
||||
"root_pipeline[%s] node(%s) activity context values: %s",
|
||||
root_pipeline_id,
|
||||
self.node.id,
|
||||
context_values,
|
||||
)
|
||||
|
||||
context = Context(self.runtime, context_values, root_pipeline_inputs)
|
||||
# hydrate will call user code, use try to catch unexpected error
|
||||
try:
|
||||
hydrated_context = context.hydrate(deformat=True)
|
||||
except Exception as e:
|
||||
logger.exception(
|
||||
"root_pipeline[%s] node(%s) activity context hydrate error",
|
||||
root_pipeline_id,
|
||||
self.node.id,
|
||||
)
|
||||
service_data = ExecutionData(inputs=data.plain_inputs(), outputs={})
|
||||
service_data.outputs.ex_data = "inputs hydrate failed(%s), check node log for details" % e
|
||||
service_data.outputs._result = False
|
||||
service_data.outputs._loop = loop
|
||||
service_data.outputs._inner_loop = inner_loop
|
||||
|
||||
self.runtime.set_execution_data(node_id=self.node.id, data=service_data)
|
||||
self.runtime.set_state(
|
||||
node_id=self.node.id,
|
||||
version=version,
|
||||
to_state=states.FAILED,
|
||||
set_archive_time=True,
|
||||
)
|
||||
return ExecuteResult(
|
||||
should_sleep=True,
|
||||
schedule_ready=False,
|
||||
schedule_type=None,
|
||||
schedule_after=-1,
|
||||
dispatch_processes=[],
|
||||
next_node_id=None,
|
||||
)
|
||||
|
||||
logger.info(
|
||||
"root_pipeline[%s] node(%s) actvity hydrated context: %s",
|
||||
root_pipeline_id,
|
||||
self.node.id,
|
||||
hydrated_context,
|
||||
)
|
||||
|
||||
# resolve inputs
|
||||
execute_inputs = Template(need_render_inputs).render(hydrated_context)
|
||||
execute_inputs.update(render_escape_inputs)
|
||||
|
||||
# data prepare
|
||||
service_data = ExecutionData(inputs=execute_inputs, outputs={})
|
||||
root_pipeline_data = ExecutionData(inputs=root_pipeline_inputs, outputs={})
|
||||
|
||||
# execute
|
||||
service = self.runtime.get_service(code=self.node.code, version=self.node.version)
|
||||
service.setup_runtime_attributes(
|
||||
id=self.node.id,
|
||||
version=version,
|
||||
top_pipeline_id=top_pipeline_id,
|
||||
root_pipeline_id=root_pipeline_id,
|
||||
loop=loop,
|
||||
inner_loop=inner_loop,
|
||||
)
|
||||
|
||||
# start monitor
|
||||
monitoring = False
|
||||
if self.node.timeout is not None:
|
||||
monitoring = True
|
||||
self.runtime.start_timeout_monitor(
|
||||
process_id=process_info.process_id,
|
||||
node_id=self.node.id,
|
||||
version=version,
|
||||
timeout=self.node.timeout,
|
||||
)
|
||||
|
||||
# pre_execute and excute
|
||||
logger.debug(
|
||||
"root_pipeline[%s] node(%s) service data before execute: %s",
|
||||
self.node.id,
|
||||
root_pipeline_id,
|
||||
service_data,
|
||||
)
|
||||
logger.debug(
|
||||
"root_pipeline[%s] node(%s) root pipeline data before execute: %s",
|
||||
self.node.id,
|
||||
root_pipeline_id,
|
||||
root_pipeline_data,
|
||||
)
|
||||
execute_success = False
|
||||
try:
|
||||
service.pre_execute(data=service_data, root_pipeline_data=root_pipeline_data)
|
||||
execute_success = service.execute(data=service_data, root_pipeline_data=root_pipeline_data)
|
||||
except Exception:
|
||||
ex_data = traceback.format_exc()
|
||||
service_data.outputs.ex_data = ex_data
|
||||
logger.warning("root_pipeline[%s]service execute fail: %s", process_info.root_pipeline_id, ex_data)
|
||||
logger.debug("root_pipeline[%s] service data after execute: %s", root_pipeline_id, service_data)
|
||||
service_data.outputs._result = execute_success
|
||||
service_data.outputs._loop = loop
|
||||
service_data.outputs._inner_loop = inner_loop
|
||||
|
||||
# execute success
|
||||
if execute_success:
|
||||
|
||||
need_schedule = service.need_schedule()
|
||||
next_node_id = None
|
||||
|
||||
if not need_schedule:
|
||||
if monitoring:
|
||||
self.runtime.stop_timeout_monitor(
|
||||
process_id=process_info.process_id,
|
||||
node_id=self.node.id,
|
||||
version=version,
|
||||
timeout=self.node.timeout,
|
||||
)
|
||||
|
||||
self.runtime.set_state(
|
||||
node_id=self.node.id,
|
||||
version=version,
|
||||
to_state=states.FINISHED,
|
||||
set_archive_time=True,
|
||||
)
|
||||
|
||||
context.extract_outputs(
|
||||
pipeline_id=top_pipeline_id,
|
||||
data_outputs=data.outputs,
|
||||
execution_data_outputs=service_data.outputs,
|
||||
)
|
||||
next_node_id = self.node.target_nodes[0]
|
||||
|
||||
self.runtime.set_execution_data(node_id=self.node.id, data=service_data)
|
||||
|
||||
return ExecuteResult(
|
||||
should_sleep=need_schedule,
|
||||
schedule_ready=need_schedule,
|
||||
schedule_type=service.schedule_type(),
|
||||
schedule_after=service.schedule_after(
|
||||
schedule=None,
|
||||
data=service_data,
|
||||
root_pipeline_data=root_pipeline_data,
|
||||
),
|
||||
dispatch_processes=[],
|
||||
next_node_id=next_node_id,
|
||||
)
|
||||
|
||||
# pre_execute failed or execute failed
|
||||
if monitoring:
|
||||
self.runtime.stop_timeout_monitor(
|
||||
process_id=process_info.process_id,
|
||||
node_id=self.node.id,
|
||||
version=version,
|
||||
timeout=self.node.timeout,
|
||||
)
|
||||
|
||||
if not self.node.error_ignorable:
|
||||
self.runtime.set_state(
|
||||
node_id=self.node.id,
|
||||
version=version,
|
||||
to_state=states.FAILED,
|
||||
set_archive_time=True,
|
||||
)
|
||||
|
||||
self.runtime.set_execution_data(node_id=self.node.id, data=service_data)
|
||||
|
||||
context.extract_outputs(
|
||||
pipeline_id=top_pipeline_id,
|
||||
data_outputs=data.outputs,
|
||||
execution_data_outputs=service_data.outputs,
|
||||
)
|
||||
|
||||
return ExecuteResult(
|
||||
should_sleep=True,
|
||||
schedule_ready=False,
|
||||
schedule_type=None,
|
||||
schedule_after=-1,
|
||||
dispatch_processes=[],
|
||||
next_node_id=None,
|
||||
)
|
||||
|
||||
# pre_execute failed or execute failed and error ignore
|
||||
self.runtime.set_state(
|
||||
node_id=self.node.id,
|
||||
version=version,
|
||||
to_state=states.FINISHED,
|
||||
set_archive_time=True,
|
||||
error_ignored=True,
|
||||
)
|
||||
|
||||
self.runtime.set_execution_data(node_id=self.node.id, data=service_data)
|
||||
|
||||
context.extract_outputs(
|
||||
pipeline_id=top_pipeline_id,
|
||||
data_outputs=data.outputs,
|
||||
execution_data_outputs=service_data.outputs,
|
||||
)
|
||||
|
||||
return ExecuteResult(
|
||||
should_sleep=False,
|
||||
schedule_ready=False,
|
||||
schedule_type=None,
|
||||
schedule_after=-1,
|
||||
dispatch_processes=[],
|
||||
next_node_id=self.node.target_nodes[0],
|
||||
)
|
||||
|
||||
def _finish_schedule(
|
||||
self,
|
||||
process_info: ProcessInfo,
|
||||
schedule: Schedule,
|
||||
data_outputs: dict,
|
||||
execution_data: ExecutionData,
|
||||
error_ignored: bool,
|
||||
root_pipeline_inputs: dict,
|
||||
) -> ScheduleResult:
|
||||
if self.node.timeout is not None:
|
||||
self.runtime.stop_timeout_monitor(
|
||||
process_id=process_info.process_id,
|
||||
node_id=self.node.id,
|
||||
version=schedule.version,
|
||||
timeout=self.node.timeout,
|
||||
)
|
||||
|
||||
self.runtime.set_state(
|
||||
node_id=self.node.id,
|
||||
version=schedule.version,
|
||||
to_state=states.FINISHED,
|
||||
set_archive_time=True,
|
||||
error_ignored=error_ignored,
|
||||
)
|
||||
|
||||
context = Context(self.runtime, [], root_pipeline_inputs)
|
||||
context.extract_outputs(
|
||||
pipeline_id=process_info.top_pipeline_id,
|
||||
data_outputs=data_outputs,
|
||||
execution_data_outputs=execution_data.outputs,
|
||||
)
|
||||
|
||||
return ScheduleResult(
|
||||
has_next_schedule=False,
|
||||
schedule_after=-1,
|
||||
schedule_done=True,
|
||||
next_node_id=self.node.target_nodes[0],
|
||||
)
|
||||
|
||||
def schedule(
|
||||
self,
|
||||
process_info: ProcessInfo,
|
||||
loop: int,
|
||||
inner_loop: int,
|
||||
schedule: Schedule,
|
||||
callback_data: Optional[CallbackData] = None,
|
||||
) -> ScheduleResult:
|
||||
"""
|
||||
节点的 schedule 处理逻辑
|
||||
|
||||
:param process_id: 进程 ID
|
||||
:type process_id: int
|
||||
:param schedule: Schedule 实例
|
||||
:type schedule: Schedule
|
||||
:param callback_data: 回调数据, defaults to None
|
||||
:type callback_data: Optional[CallbackData], optional
|
||||
:return: 调度结果
|
||||
:rtype: ScheduleResult
|
||||
"""
|
||||
# data prepare
|
||||
top_pipeline_id = process_info.top_pipeline_id
|
||||
root_pipeline_id = process_info.root_pipeline_id
|
||||
|
||||
data_outputs = self.runtime.get_data_outputs(self.node.id)
|
||||
service_data = self.runtime.get_execution_data(self.node.id)
|
||||
|
||||
root_pipeline_inputs = self._get_plain_inputs(root_pipeline_id)
|
||||
root_pipeline_data = ExecutionData(inputs=root_pipeline_inputs, outputs={})
|
||||
logger.info(
|
||||
"root_pipeline[%s] node(%s) activity schedule data: %s, root inputs: %s",
|
||||
root_pipeline_id,
|
||||
self.node.id,
|
||||
service_data,
|
||||
root_pipeline_inputs,
|
||||
)
|
||||
|
||||
# schedule
|
||||
service = self.runtime.get_service(code=self.node.code, version=self.node.version)
|
||||
service.setup_runtime_attributes(
|
||||
id=self.node.id,
|
||||
version=schedule.version,
|
||||
top_pipeline_id=top_pipeline_id,
|
||||
root_pipeline_id=root_pipeline_id,
|
||||
loop=loop,
|
||||
inner_loop=inner_loop,
|
||||
)
|
||||
|
||||
schedule_success = False
|
||||
schedule.times += 1
|
||||
try:
|
||||
schedule_success = service.schedule(
|
||||
schedule=schedule,
|
||||
data=service_data,
|
||||
root_pipeline_data=root_pipeline_data,
|
||||
callback_data=callback_data,
|
||||
)
|
||||
except Exception:
|
||||
service_data.outputs.ex_data = traceback.format_exc()
|
||||
|
||||
service_data.outputs._result = schedule_success
|
||||
service_data.outputs._loop = loop
|
||||
service_data.outputs._inner_loop = inner_loop
|
||||
|
||||
self.runtime.add_schedule_times(schedule.id)
|
||||
self.runtime.set_execution_data(node_id=self.node.id, data=service_data)
|
||||
|
||||
monitoring = self.node.timeout is not None
|
||||
schedule_type = service.schedule_type()
|
||||
|
||||
# schedule success
|
||||
if schedule_success:
|
||||
if schedule_type == ScheduleType.CALLBACK:
|
||||
return self._finish_schedule(
|
||||
process_info=process_info,
|
||||
schedule=schedule,
|
||||
data_outputs=data_outputs,
|
||||
execution_data=service_data,
|
||||
error_ignored=False,
|
||||
root_pipeline_inputs=root_pipeline_inputs,
|
||||
)
|
||||
else:
|
||||
is_schedule_done = service.is_schedule_done()
|
||||
|
||||
# poll or multi-callback finished
|
||||
if is_schedule_done:
|
||||
return self._finish_schedule(
|
||||
process_info=process_info,
|
||||
schedule=schedule,
|
||||
data_outputs=data_outputs,
|
||||
execution_data=service_data,
|
||||
error_ignored=False,
|
||||
root_pipeline_inputs=root_pipeline_inputs,
|
||||
)
|
||||
|
||||
has_next_schedule = schedule_type == ScheduleType.POLL
|
||||
return ScheduleResult(
|
||||
has_next_schedule=has_next_schedule,
|
||||
schedule_after=service.schedule_after(
|
||||
schedule=schedule,
|
||||
data=service_data,
|
||||
root_pipeline_data=root_pipeline_data,
|
||||
),
|
||||
schedule_done=False,
|
||||
next_node_id=None,
|
||||
)
|
||||
|
||||
if monitoring:
|
||||
self.runtime.stop_timeout_monitor(
|
||||
process_id=process_info.process_id,
|
||||
node_id=self.node.id,
|
||||
version=schedule.version,
|
||||
timeout=self.node.timeout,
|
||||
)
|
||||
|
||||
# schedule fail
|
||||
if not self.node.error_ignorable:
|
||||
self.runtime.set_state(
|
||||
node_id=self.node.id,
|
||||
version=schedule.version,
|
||||
to_state=states.FAILED,
|
||||
set_archive_time=True,
|
||||
)
|
||||
|
||||
context = Context(self.runtime, [], root_pipeline_inputs)
|
||||
context.extract_outputs(
|
||||
pipeline_id=process_info.top_pipeline_id,
|
||||
data_outputs=data_outputs,
|
||||
execution_data_outputs=service_data.outputs,
|
||||
)
|
||||
|
||||
return ScheduleResult(
|
||||
has_next_schedule=False,
|
||||
schedule_after=-1,
|
||||
schedule_done=False,
|
||||
next_node_id=None,
|
||||
)
|
||||
|
||||
# schedule fail and error ignore
|
||||
return self._finish_schedule(
|
||||
process_info=process_info,
|
||||
schedule=schedule,
|
||||
data_outputs=data_outputs,
|
||||
execution_data=service_data,
|
||||
error_ignored=True,
|
||||
root_pipeline_inputs=root_pipeline_inputs,
|
||||
)
|
||||
129
lib/bamboo_engine/handlers/subprocess.py
Normal file
129
lib/bamboo_engine/handlers/subprocess.py
Normal file
@@ -0,0 +1,129 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS Community
|
||||
Edition) available.
|
||||
Copyright (C) 2017-2021 THL A29 Limited, a Tencent company. All rights reserved.
|
||||
Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
http://opensource.org/licenses/MIT
|
||||
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 logging
|
||||
|
||||
from bamboo_engine.context import Context
|
||||
from bamboo_engine.config import Settings
|
||||
from bamboo_engine.template import Template
|
||||
from bamboo_engine.eri import ProcessInfo, ContextValue, ContextValueType, NodeType
|
||||
from bamboo_engine.handler import register_handler, NodeHandler, ExecuteResult
|
||||
|
||||
logger = logging.getLogger("bamboo_engine")
|
||||
|
||||
|
||||
@register_handler(NodeType.SubProcess)
|
||||
class SubProcessHandler(NodeHandler):
|
||||
def execute(self, process_info: ProcessInfo, loop: int, inner_loop: int, version: str) -> ExecuteResult:
|
||||
"""
|
||||
节点的 execute 处理逻辑
|
||||
|
||||
:param runtime: 引擎运行时实例
|
||||
:type runtime: EngineRuntimeInterface
|
||||
:param process_info: 进程信息
|
||||
:type process_id: ProcessInfo
|
||||
:return: 执行结果
|
||||
:rtype: ExecuteResult
|
||||
"""
|
||||
data = self.runtime.get_data(self.node.id)
|
||||
root_pipeline_inputs = self._get_plain_inputs(process_info.root_pipeline_id)
|
||||
need_render_inputs = data.need_render_inputs()
|
||||
render_escape_inputs = data.render_escape_inputs()
|
||||
top_pipeline_id = process_info.top_pipeline_id
|
||||
root_pipeline_id = process_info.root_pipeline_id
|
||||
|
||||
logger.info(
|
||||
"root_pipeline[%s] node(%s) subprocess data: %s",
|
||||
root_pipeline_id,
|
||||
self.node.id,
|
||||
data,
|
||||
)
|
||||
|
||||
# reset inner_loop of nodes in subprocess
|
||||
self.runtime.reset_children_state_inner_loop(self.node.id)
|
||||
|
||||
# resolve inputs context references
|
||||
inputs_refs = Template(need_render_inputs).get_reference()
|
||||
logger.info(
|
||||
"root_pipeline[%s] node(%s) subprocess original refs: %s",
|
||||
root_pipeline_id,
|
||||
self.node.id,
|
||||
inputs_refs,
|
||||
)
|
||||
|
||||
additional_refs = self.runtime.get_context_key_references(pipeline_id=top_pipeline_id, keys=inputs_refs)
|
||||
inputs_refs = inputs_refs.union(additional_refs)
|
||||
logger.info(
|
||||
"root_pipeline[%s] node(%s) subprocess final refs: %s",
|
||||
root_pipeline_id,
|
||||
self.node.id,
|
||||
inputs_refs,
|
||||
)
|
||||
|
||||
# prepare context
|
||||
context_values = self.runtime.get_context_values(pipeline_id=top_pipeline_id, keys=inputs_refs)
|
||||
|
||||
# pre extract loop outputs
|
||||
loop_value = loop + Settings.RERUN_INDEX_OFFSET
|
||||
if self.LOOP_KEY in data.outputs:
|
||||
loop_output_key = data.outputs[self.LOOP_KEY]
|
||||
context_values.append(
|
||||
ContextValue(
|
||||
key=loop_output_key,
|
||||
type=ContextValueType.PLAIN,
|
||||
value=loop_value,
|
||||
)
|
||||
)
|
||||
logger.info(
|
||||
"root_pipeline[%s] node(%s) subprocess parent context values: %s",
|
||||
root_pipeline_id,
|
||||
self.node.id,
|
||||
context_values,
|
||||
)
|
||||
|
||||
context = Context(self.runtime, context_values, root_pipeline_inputs)
|
||||
hydrated_context = context.hydrate(deformat=True)
|
||||
logger.info(
|
||||
"root_pipeline[%s] node(%s) subprocess parent hydrated context: %s",
|
||||
root_pipeline_id,
|
||||
self.node.id,
|
||||
hydrated_context,
|
||||
)
|
||||
|
||||
# resolve inputs
|
||||
subprocess_inputs = Template(need_render_inputs).render(hydrated_context)
|
||||
subprocess_inputs.update(render_escape_inputs)
|
||||
sub_context_values = {
|
||||
key: ContextValue(key=key, type=ContextValueType.PLAIN, value=value)
|
||||
for key, value in subprocess_inputs.items()
|
||||
}
|
||||
logger.info(
|
||||
"root_pipeline[%s] node(%s) subprocess inject context: %s",
|
||||
root_pipeline_id,
|
||||
self.node.id,
|
||||
sub_context_values,
|
||||
)
|
||||
|
||||
# update subprocess context, inject subprocess data
|
||||
self.runtime.upsert_plain_context_values(self.node.id, sub_context_values)
|
||||
process_info.pipeline_stack.append(self.node.id)
|
||||
self.runtime.set_pipeline_stack(process_info.process_id, process_info.pipeline_stack)
|
||||
|
||||
return ExecuteResult(
|
||||
should_sleep=False,
|
||||
schedule_ready=False,
|
||||
schedule_type=None,
|
||||
schedule_after=-1,
|
||||
dispatch_processes=[],
|
||||
next_node_id=self.node.start_event_id,
|
||||
)
|
||||
62
lib/bamboo_engine/local.py
Normal file
62
lib/bamboo_engine/local.py
Normal file
@@ -0,0 +1,62 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS Community
|
||||
Edition) available.
|
||||
Copyright (C) 2017-2021 THL A29 Limited, a Tencent company. All rights reserved.
|
||||
Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
http://opensource.org/licenses/MIT
|
||||
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.
|
||||
"""
|
||||
|
||||
|
||||
# 引擎执行 local
|
||||
|
||||
|
||||
from typing import Optional
|
||||
|
||||
from werkzeug.local import Local
|
||||
|
||||
from .utils.object import Representable
|
||||
|
||||
_local = Local()
|
||||
|
||||
|
||||
class CurrentNodeInfo(Representable):
|
||||
def __init__(self, node_id: str, version: str, loop: int):
|
||||
self.node_id = node_id
|
||||
self.version = version
|
||||
self.loop = loop
|
||||
|
||||
|
||||
def set_node_info(node_info: CurrentNodeInfo):
|
||||
"""
|
||||
设置当前进程/线程/协程 Local 中的当前节点信息
|
||||
|
||||
:param node_id: 节点 ID
|
||||
:type node_id: str
|
||||
:param version: 节点版本
|
||||
:type version: str
|
||||
:param loop: 重入次数
|
||||
:type loop: int
|
||||
"""
|
||||
_local.current_node_info = node_info
|
||||
|
||||
|
||||
def get_node_info() -> Optional[CurrentNodeInfo]:
|
||||
"""
|
||||
获取当前进程/线程/协程正在处理的节点 ID,版本及重入次数
|
||||
|
||||
:return: 节点 ID
|
||||
:rtype: [type]
|
||||
"""
|
||||
return getattr(_local, "current_node_info", None)
|
||||
|
||||
|
||||
def clear_node_info():
|
||||
"""
|
||||
清理当前进程/线程/协程 Local 中的当前节点信息
|
||||
"""
|
||||
_local.current_node_info = None
|
||||
194
lib/bamboo_engine/metrics.py
Normal file
194
lib/bamboo_engine/metrics.py
Normal file
@@ -0,0 +1,194 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS Community
|
||||
Edition) available.
|
||||
Copyright (C) 2017-2021 THL A29 Limited, a Tencent company. All rights reserved.
|
||||
Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
http://opensource.org/licenses/MIT
|
||||
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 time
|
||||
from functools import wraps
|
||||
|
||||
from prometheus_client import Gauge, Histogram
|
||||
|
||||
from .utils.host import get_hostname
|
||||
|
||||
HOST_NAME = get_hostname()
|
||||
|
||||
|
||||
def decode_buckets(buckets_list):
|
||||
return [float(x) for x in buckets_list.split(",")]
|
||||
|
||||
|
||||
def get_histogram_buckets_from_evn(env_name):
|
||||
if env_name in os.environ:
|
||||
buckets = decode_buckets(os.environ.get(env_name))
|
||||
else:
|
||||
if hasattr(Histogram, "DEFAULT_BUCKETS"): # pragma: no cover
|
||||
buckets = Histogram.DEFAULT_BUCKETS
|
||||
else: # pragma: no cover
|
||||
# For prometheus-client < 0.3.0 we cannot easily access
|
||||
# the default buckets:
|
||||
buckets = (
|
||||
0.005,
|
||||
0.01,
|
||||
0.025,
|
||||
0.05,
|
||||
0.075,
|
||||
0.1,
|
||||
0.25,
|
||||
0.5,
|
||||
0.75,
|
||||
1.0,
|
||||
2.5,
|
||||
5.0,
|
||||
7.5,
|
||||
10.0,
|
||||
float("inf"),
|
||||
)
|
||||
return buckets
|
||||
|
||||
|
||||
def setup_gauge(*gauges):
|
||||
def wrapper(func):
|
||||
@wraps(func)
|
||||
def _wrapper(*args, **kwargs):
|
||||
for g in gauges:
|
||||
g.labels(hostname=HOST_NAME).inc(1)
|
||||
try:
|
||||
return func(*args, **kwargs)
|
||||
finally:
|
||||
for g in gauges:
|
||||
g.labels(hostname=HOST_NAME).dec(1)
|
||||
|
||||
return _wrapper
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
def setup_histogram(*histograms):
|
||||
def wrapper(func):
|
||||
@wraps(func)
|
||||
def _wrapper(*args, **kwargs):
|
||||
start = time.time()
|
||||
try:
|
||||
return func(*args, **kwargs)
|
||||
finally:
|
||||
for h in histograms:
|
||||
h.labels(hostname=HOST_NAME).observe(time.time() - start)
|
||||
|
||||
return _wrapper
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
# engine metrics
|
||||
ENGINE_RUNNING_PROCESSES = Gauge("engine_running_processes", "count running state processes", labelnames=["hostname"])
|
||||
ENGINE_RUNNING_SCHEDULES = Gauge("engine_running_schedules", "count running state schedules", labelnames=["hostname"])
|
||||
ENGINE_PROCESS_RUNNING_TIME = Histogram(
|
||||
"engine_process_running_time",
|
||||
"time spent running process",
|
||||
buckets=get_histogram_buckets_from_evn("ENGINE_PROCESS_RUNNING_TIME_BUCKETS"),
|
||||
labelnames=["hostname"],
|
||||
)
|
||||
ENGINE_SCHEDULE_RUNNING_TIME = Histogram(
|
||||
"engine_schedule_running_time",
|
||||
"time spent running schedule",
|
||||
buckets=get_histogram_buckets_from_evn("ENGINE_SCHEDULE_RUNNING_TIME_BUCKETS"),
|
||||
labelnames=["hostname"],
|
||||
)
|
||||
ENGINE_NODE_EXECUTE_TIME = Histogram(
|
||||
"engine_node_execute_time",
|
||||
"time spent executing node",
|
||||
buckets=get_histogram_buckets_from_evn("ENGINE_NODE_EXECUTE_TIME_BUCKETS"),
|
||||
labelnames=["type", "hostname"],
|
||||
)
|
||||
ENGINE_NODE_SCHEDULE_TIME = Histogram(
|
||||
"engine_node_schedule_time",
|
||||
"time spent scheduling node",
|
||||
buckets=get_histogram_buckets_from_evn("ENGINE_NODE_SCHEDULE_TIME_BUCKETS"),
|
||||
labelnames=["type", "hostname"],
|
||||
)
|
||||
|
||||
# runtime metrics
|
||||
ENGINE_RUNTIME_CONTEXT_VALUE_READ_TIME = Histogram(
|
||||
"engine_runtime_context_value_read_time", "time spent reading context value", labelnames=["hostname"]
|
||||
)
|
||||
ENGINE_RUNTIME_CONTEXT_REF_READ_TIME = Histogram(
|
||||
"engine_runtime_context_ref_read_time", "time spent reading context value reference", labelnames=["hostname"]
|
||||
)
|
||||
ENGINE_RUNTIME_CONTEXT_VALUE_UPSERT_TIME = Histogram(
|
||||
"engine_runtime_context_value_upsert_time", "time spent upserting context value", labelnames=["hostname"]
|
||||
)
|
||||
|
||||
ENGINE_RUNTIME_DATA_INPUTS_READ_TIME = Histogram(
|
||||
"engine_runtime_data_inputs_read_time", "time spent reading node data inputs", labelnames=["hostname"]
|
||||
)
|
||||
ENGINE_RUNTIME_DATA_OUTPUTS_READ_TIME = Histogram(
|
||||
"engine_runtime_data_outputs_read_time", "time spent reading node data outputs", labelnames=["hostname"]
|
||||
)
|
||||
ENGINE_RUNTIME_DATA_READ_TIME = Histogram(
|
||||
"engine_runtime_data_read_time", "time spent reading node data inputs and outputs", labelnames=["hostname"]
|
||||
)
|
||||
|
||||
ENGINE_RUNTIME_EXEC_DATA_INPUTS_READ_TIME = Histogram(
|
||||
"engine_runtime_exec_data_inputs_read_time",
|
||||
"time spent reading node execution data inputs",
|
||||
labelnames=["hostname"],
|
||||
)
|
||||
ENGINE_RUNTIME_EXEC_DATA_OUTPUTS_READ_TIME = Histogram(
|
||||
"engine_runtime_exec_data_outputs_read_time",
|
||||
"time spent reading node execution data outputs",
|
||||
labelnames=["hostname"],
|
||||
)
|
||||
ENGINE_RUNTIME_EXEC_DATA_READ_TIME = Histogram(
|
||||
"engine_runtime_exec_data_read_time",
|
||||
"time spent reading node execution data inputs and outputs",
|
||||
labelnames=["hostname"],
|
||||
)
|
||||
ENGINE_RUNTIME_EXEC_DATA_INPUTS_WRITE_TIME = Histogram(
|
||||
"engine_runtime_exec_data_inputs_write_time",
|
||||
"time spent writing node execution data inputs",
|
||||
labelnames=["hostname"],
|
||||
)
|
||||
ENGINE_RUNTIME_EXEC_DATA_OUTPUTS_WRITE_TIME = Histogram(
|
||||
"engine_runtime_exec_data_outputs_write_time",
|
||||
"time spent writing node execution data outputs",
|
||||
labelnames=["hostname"],
|
||||
)
|
||||
ENGINE_RUNTIME_EXEC_DATA_WRITE_TIME = Histogram(
|
||||
"engine_runtime_exec_data_write_time",
|
||||
"time spent writing node execution data inputs and outputs",
|
||||
labelnames=["hostname"],
|
||||
)
|
||||
ENGINE_RUNTIME_CALLBACK_DATA_READ_TIME = Histogram(
|
||||
"engine_runtime_callback_data_read_time", "time spent reading node callback data", labelnames=["hostname"]
|
||||
)
|
||||
|
||||
ENGINE_RUNTIME_SCHEDULE_READ_TIME = Histogram(
|
||||
"engine_runtime_schedule_read_time", "time spent reading schedule", labelnames=["hostname"]
|
||||
)
|
||||
ENGINE_RUNTIME_SCHEDULE_WRITE_TIME = Histogram(
|
||||
"engine_runtime_schedule_write_time", "time spent writing schedule", labelnames=["hostname"]
|
||||
)
|
||||
|
||||
ENGINE_RUNTIME_STATE_READ_TIME = Histogram(
|
||||
"engine_runtime_state_read_time", "time spent reading state", labelnames=["hostname"]
|
||||
)
|
||||
ENGINE_RUNTIME_STATE_WRITE_TIME = Histogram(
|
||||
"engine_runtime_state_write_time", "time spent writing state", labelnames=["hostname"]
|
||||
)
|
||||
|
||||
ENGINE_RUNTIME_NODE_READ_TIME = Histogram(
|
||||
"engine_runtime_node_read_time", "time spent reading node", labelnames=["hostname"]
|
||||
)
|
||||
|
||||
ENGINE_RUNTIME_PROCESS_READ_TIME = Histogram(
|
||||
"engine_runtime_process_read_time", "time spent reading process", labelnames=["hostname"]
|
||||
)
|
||||
69
lib/bamboo_engine/states.py
Normal file
69
lib/bamboo_engine/states.py
Normal file
@@ -0,0 +1,69 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS Community
|
||||
Edition) available.
|
||||
Copyright (C) 2017-2021 THL A29 Limited, a Tencent company. All rights reserved.
|
||||
Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
http://opensource.org/licenses/MIT
|
||||
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.
|
||||
"""
|
||||
|
||||
|
||||
# 引擎内部状态及状态相关数据定义模块
|
||||
|
||||
|
||||
from enum import Enum
|
||||
|
||||
from .utils.collections import ConstantDict
|
||||
|
||||
|
||||
class StateType(Enum):
|
||||
CREATED = "CREATED"
|
||||
READY = "READY"
|
||||
RUNNING = "RUNNING"
|
||||
SUSPENDED = "SUSPENDED"
|
||||
BLOCKED = "BLOCKED"
|
||||
FINISHED = "FINISHED"
|
||||
FAILED = "FAILED"
|
||||
REVOKED = "REVOKED"
|
||||
|
||||
|
||||
CREATED = StateType.CREATED.value
|
||||
READY = StateType.READY.value
|
||||
RUNNING = StateType.RUNNING.value
|
||||
SUSPENDED = StateType.SUSPENDED.value
|
||||
BLOCKED = StateType.BLOCKED.value
|
||||
FINISHED = StateType.FINISHED.value
|
||||
FAILED = StateType.FAILED.value
|
||||
REVOKED = StateType.REVOKED.value
|
||||
|
||||
ALL_STATES = frozenset([READY, RUNNING, SUSPENDED, BLOCKED, FINISHED, FAILED, REVOKED])
|
||||
|
||||
ARCHIVED_STATES = frozenset([FINISHED, FAILED, REVOKED])
|
||||
SLEEP_STATES = frozenset([SUSPENDED, REVOKED])
|
||||
CHILDREN_IGNORE_STATES = frozenset([BLOCKED])
|
||||
|
||||
INVERTED_TRANSITION = ConstantDict({RUNNING: frozenset([READY, FINISHED])})
|
||||
|
||||
TRANSITION = ConstantDict(
|
||||
{
|
||||
READY: frozenset([RUNNING, SUSPENDED]),
|
||||
RUNNING: frozenset([FINISHED, FAILED, REVOKED, SUSPENDED]),
|
||||
SUSPENDED: frozenset([READY, REVOKED, RUNNING]),
|
||||
BLOCKED: frozenset([]),
|
||||
FINISHED: frozenset([RUNNING, FAILED]),
|
||||
FAILED: frozenset([READY, FINISHED]),
|
||||
REVOKED: frozenset([]),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def can_transit(from_state, to_state):
|
||||
|
||||
if from_state in TRANSITION:
|
||||
if to_state in TRANSITION[from_state]:
|
||||
return True
|
||||
return False
|
||||
18
lib/bamboo_engine/template/__init__.py
Normal file
18
lib/bamboo_engine/template/__init__.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS Community
|
||||
Edition) available.
|
||||
Copyright (C) 2017-2021 THL A29 Limited, a Tencent company. All rights reserved.
|
||||
Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
http://opensource.org/licenses/MIT
|
||||
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.
|
||||
"""
|
||||
|
||||
"""
|
||||
模板相关逻辑存放模块
|
||||
"""
|
||||
|
||||
from .template import Template # noqa
|
||||
54
lib/bamboo_engine/template/sandbox.py
Normal file
54
lib/bamboo_engine/template/sandbox.py
Normal file
@@ -0,0 +1,54 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS Community
|
||||
Edition) available.
|
||||
Copyright (C) 2017-2021 THL A29 Limited, a Tencent company. All rights reserved.
|
||||
Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
http://opensource.org/licenses/MIT
|
||||
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.
|
||||
"""
|
||||
|
||||
|
||||
# 模板渲染沙箱
|
||||
|
||||
|
||||
from typing import List, Dict
|
||||
|
||||
import importlib
|
||||
|
||||
from bamboo_engine.config import Settings
|
||||
|
||||
|
||||
def _shield_words(sandbox: dict, words: List[str]):
|
||||
for shield_word in words:
|
||||
sandbox[shield_word] = None
|
||||
|
||||
|
||||
class ModuleObject:
|
||||
def __init__(self, sub_paths, module):
|
||||
if len(sub_paths) == 1:
|
||||
setattr(self, sub_paths[0], module)
|
||||
return
|
||||
setattr(self, sub_paths[0], ModuleObject(sub_paths[1:], module))
|
||||
|
||||
|
||||
def _import_modules(sandbox: dict, modules: Dict[str, str]):
|
||||
for mod_path, alias in modules.items():
|
||||
mod = importlib.import_module(mod_path)
|
||||
sub_paths = alias.split(".")
|
||||
if len(sub_paths) == 1:
|
||||
sandbox[alias] = mod
|
||||
else:
|
||||
sandbox[sub_paths[0]] = ModuleObject(sub_paths[1:], mod)
|
||||
|
||||
|
||||
def get() -> dict:
|
||||
sandbox = {}
|
||||
|
||||
_shield_words(sandbox, Settings.MAKO_SANDBOX_SHIELD_WORDS)
|
||||
_import_modules(sandbox, Settings.MAKO_SANDBOX_IMPORT_MODULES)
|
||||
|
||||
return sandbox
|
||||
195
lib/bamboo_engine/template/template.py
Normal file
195
lib/bamboo_engine/template/template.py
Normal file
@@ -0,0 +1,195 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS Community
|
||||
Edition) available.
|
||||
Copyright (C) 2017-2021 THL A29 Limited, a Tencent company. All rights reserved.
|
||||
Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
http://opensource.org/licenses/MIT
|
||||
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 copy
|
||||
import re
|
||||
import logging
|
||||
|
||||
from typing import Any, List, Set
|
||||
|
||||
from mako.template import Template as MakoTemplate
|
||||
from mako import lexer, codegen
|
||||
from mako.exceptions import MakoException
|
||||
|
||||
from bamboo_engine.utils.mako_utils.checker import check_mako_template_safety
|
||||
from bamboo_engine.utils.mako_utils.exceptions import ForbiddenMakoTemplateException
|
||||
from bamboo_engine.utils import mako_safety
|
||||
from bamboo_engine.utils.string import deformat_var_key
|
||||
|
||||
from . import sandbox
|
||||
|
||||
|
||||
logger = logging.getLogger("root")
|
||||
# find mako template(format is ${xxx},and ${}# not in xxx, # may raise memory error)
|
||||
TEMPLATE_PATTERN = re.compile(r"\${[^${}#]+}")
|
||||
|
||||
|
||||
class Template:
|
||||
def __init__(self, data: Any):
|
||||
self.data = data
|
||||
|
||||
def get_reference(self, deformat=False) -> Set[str]:
|
||||
"""
|
||||
获取当前数据中模板所引用的所有标志符
|
||||
|
||||
:return: 标志符列表
|
||||
:rtype: List[str]
|
||||
"""
|
||||
|
||||
reference = []
|
||||
templates = self.get_templates()
|
||||
for tpl in templates:
|
||||
reference += self._get_template_reference(tpl)
|
||||
reference = set(reference)
|
||||
if not deformat:
|
||||
reference = {"${%s}" % r for r in reference}
|
||||
|
||||
return reference
|
||||
|
||||
def get_templates(self) -> List[str]:
|
||||
"""
|
||||
获取当前数据中所有的模板片段
|
||||
|
||||
:return: 模板片段列表
|
||||
:rtype: List[str]
|
||||
"""
|
||||
templates = []
|
||||
data = self.data
|
||||
if isinstance(data, str):
|
||||
templates += self._get_string_templates(data)
|
||||
if isinstance(data, (list, tuple)):
|
||||
for item in data:
|
||||
templates += Template(item).get_templates()
|
||||
if isinstance(data, dict):
|
||||
for value in list(data.values()):
|
||||
templates += Template(value).get_templates()
|
||||
return list(set(templates))
|
||||
|
||||
def render(self, context: dict) -> Any:
|
||||
"""
|
||||
渲染当前模板
|
||||
|
||||
:param context: 模板渲染上下文
|
||||
:type context: dict
|
||||
:return: 模板渲染后的数据
|
||||
:rtype: Any
|
||||
"""
|
||||
data = self.data
|
||||
if isinstance(data, str):
|
||||
return self._render_string(data, context)
|
||||
if isinstance(data, list):
|
||||
ldata = [""] * len(data)
|
||||
for index, item in enumerate(data):
|
||||
ldata[index] = Template(copy.deepcopy(item)).render(context)
|
||||
return ldata
|
||||
if isinstance(data, tuple):
|
||||
ldata = [""] * len(data)
|
||||
for index, item in enumerate(data):
|
||||
ldata[index] = Template(copy.deepcopy(item)).render(context)
|
||||
return tuple(ldata)
|
||||
if isinstance(data, dict):
|
||||
for key, value in list(data.items()):
|
||||
data[key] = Template(copy.deepcopy(value)).render(context)
|
||||
return data
|
||||
return data
|
||||
|
||||
def _get_string_templates(self, string) -> List[str]:
|
||||
return list(set(TEMPLATE_PATTERN.findall(string)))
|
||||
|
||||
def _get_template_reference(self, template: str) -> List[str]:
|
||||
lex = lexer.Lexer(template)
|
||||
|
||||
try:
|
||||
node = lex.parse()
|
||||
except MakoException as e:
|
||||
logger.warning("pipeline get template[{}] reference error[{}]".format(template, e))
|
||||
return []
|
||||
|
||||
# Dummy compiler. _Identifiers class requires one
|
||||
# but only interested in the reserved_names field
|
||||
def compiler():
|
||||
return None
|
||||
|
||||
compiler.reserved_names = set()
|
||||
identifiers = codegen._Identifiers(compiler, node)
|
||||
|
||||
return list(identifiers.undeclared)
|
||||
|
||||
def _render_string(self, string: str, context: dict) -> str:
|
||||
"""
|
||||
使用特定上下文渲染指定模板
|
||||
|
||||
:param string: 模板
|
||||
:type string: str
|
||||
:param context: 上下文
|
||||
:type context: dict
|
||||
:return: 渲染后的模板
|
||||
:rtype: str
|
||||
"""
|
||||
if not isinstance(string, str):
|
||||
return string
|
||||
templates = self._get_string_templates(string)
|
||||
|
||||
# TODO keep render return object, here only process simple situation
|
||||
if len(templates) == 1 and templates[0] == string and deformat_var_key(string) in context:
|
||||
return context[deformat_var_key(string)]
|
||||
|
||||
for tpl in templates:
|
||||
try:
|
||||
check_mako_template_safety(
|
||||
tpl,
|
||||
mako_safety.SingleLineNodeVisitor(),
|
||||
mako_safety.SingleLinCodeExtractor(),
|
||||
)
|
||||
except ForbiddenMakoTemplateException as e:
|
||||
logger.warning("forbidden template: {}, exception: {}".format(tpl, e))
|
||||
continue
|
||||
except Exception:
|
||||
logger.exception("{} safety check error.".format(tpl))
|
||||
continue
|
||||
resolved = Template._render_template(tpl, context)
|
||||
string = string.replace(tpl, resolved)
|
||||
return string
|
||||
|
||||
@staticmethod
|
||||
def _render_template(template: str, context: dict) -> Any:
|
||||
"""
|
||||
使用特定上下文渲染指定模板
|
||||
|
||||
:param template: 模板
|
||||
:type template: Any
|
||||
:param context: 上下文
|
||||
:type context: dict
|
||||
:raises TypeError: [description]
|
||||
:return: [description]
|
||||
:rtype: str
|
||||
"""
|
||||
data = {}
|
||||
data.update(sandbox.get())
|
||||
data.update(context)
|
||||
if not isinstance(template, str):
|
||||
raise TypeError("constant resolve error, template[%s] is not a string" % template)
|
||||
try:
|
||||
tm = MakoTemplate(template)
|
||||
except (MakoException, SyntaxError) as e:
|
||||
logger.error("pipeline resolve template[{}] error[{}]".format(template, e))
|
||||
return template
|
||||
try:
|
||||
resolved = tm.render_unicode(**data)
|
||||
except Exception as e:
|
||||
logger.warning("constant content({}) is invalid, data({}), error: {}".format(template, data, e))
|
||||
return template
|
||||
else:
|
||||
return resolved
|
||||
16
lib/bamboo_engine/utils/__init__.py
Normal file
16
lib/bamboo_engine/utils/__init__.py
Normal file
@@ -0,0 +1,16 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS Community
|
||||
Edition) available.
|
||||
Copyright (C) 2017-2021 THL A29 Limited, a Tencent company. All rights reserved.
|
||||
Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
http://opensource.org/licenses/MIT
|
||||
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.
|
||||
"""
|
||||
|
||||
"""
|
||||
引擎内部使用的各类工具存放模块
|
||||
"""
|
||||
22
lib/bamboo_engine/utils/boolrule/__init__.py
Normal file
22
lib/bamboo_engine/utils/boolrule/__init__.py
Normal file
@@ -0,0 +1,22 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS Community
|
||||
Edition) available.
|
||||
Copyright (C) 2017-2021 THL A29 Limited, a Tencent company. All rights reserved.
|
||||
Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
http://opensource.org/licenses/MIT
|
||||
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.
|
||||
"""
|
||||
|
||||
|
||||
# bool 表达式解析工具模块
|
||||
|
||||
|
||||
from .boolrule import ( # noqa
|
||||
BoolRule,
|
||||
MissingVariableException,
|
||||
UnknownOperatorException,
|
||||
)
|
||||
291
lib/bamboo_engine/utils/boolrule/boolrule.py
Normal file
291
lib/bamboo_engine/utils/boolrule/boolrule.py
Normal file
@@ -0,0 +1,291 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS Community
|
||||
Edition) available.
|
||||
Copyright (C) 2017-2021 THL A29 Limited, a Tencent company. All rights reserved.
|
||||
Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
http://opensource.org/licenses/MIT
|
||||
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.
|
||||
"""
|
||||
|
||||
from pyparsing import (
|
||||
CaselessLiteral,
|
||||
Combine,
|
||||
Forward,
|
||||
Group,
|
||||
Keyword,
|
||||
Optional,
|
||||
ParseException,
|
||||
ParseResults,
|
||||
QuotedString,
|
||||
Suppress,
|
||||
Word,
|
||||
ZeroOrMore,
|
||||
alphanums,
|
||||
alphas,
|
||||
delimitedList,
|
||||
nums,
|
||||
oneOf,
|
||||
)
|
||||
|
||||
PATH_DELIMITER = "."
|
||||
|
||||
|
||||
class SubstituteVal(object):
|
||||
"""
|
||||
Represents a token that will later be replaced by a context value.
|
||||
"""
|
||||
|
||||
def __init__(self, t):
|
||||
self._path = t[0]
|
||||
|
||||
def get_val(self, context):
|
||||
if not context:
|
||||
# raise MissingVariableException(
|
||||
# 'context missing or empty'
|
||||
# )
|
||||
return self._path
|
||||
|
||||
val = context
|
||||
|
||||
try:
|
||||
for part in self._path.split(PATH_DELIMITER):
|
||||
val = getattr(val, part) if hasattr(val, part) else val[part]
|
||||
|
||||
except KeyError:
|
||||
raise MissingVariableException("no value supplied for {}".format(self._path))
|
||||
|
||||
return val
|
||||
|
||||
def __repr__(self):
|
||||
return "SubstituteVal(%s)" % self._path
|
||||
|
||||
|
||||
def get_bool_expression():
|
||||
|
||||
# Grammar definition
|
||||
# match gcloud's variable
|
||||
identifier = Combine(Optional("${") + Optional("_") + Word(alphas, alphanums + "_") + Optional("}"))
|
||||
# identifier = Word(alphas, alphanums + "_")
|
||||
propertyPath = delimitedList(identifier, PATH_DELIMITER, combine=True)
|
||||
|
||||
and_ = Keyword("and", caseless=True)
|
||||
or_ = Keyword("or", caseless=True)
|
||||
|
||||
lparen = Suppress("(")
|
||||
rparen = Suppress(")")
|
||||
|
||||
binaryOp = oneOf("== != < > >= <= in notin issuperset notissuperset", caseless=True)("operator")
|
||||
|
||||
E = CaselessLiteral("E")
|
||||
numberSign = Word("+-", exact=1)
|
||||
realNumber = Combine(
|
||||
Optional(numberSign)
|
||||
+ (Word(nums) + "." + Optional(Word(nums)) | ("." + Word(nums)))
|
||||
+ Optional(E + Optional(numberSign) + Word(nums))
|
||||
)
|
||||
|
||||
integer = Combine(Optional(numberSign) + Word(nums) + Optional(E + Optional("+") + Word(nums)))
|
||||
|
||||
# str_ = quotedString.addParseAction(removeQuotes)
|
||||
str_ = QuotedString('"') | QuotedString("'")
|
||||
bool_ = oneOf("true false", caseless=True)
|
||||
|
||||
simpleVals = (
|
||||
realNumber.setParseAction(lambda toks: float(toks[0]))
|
||||
| integer.setParseAction(lambda toks: int(toks[0]))
|
||||
| str_
|
||||
| bool_.setParseAction(lambda toks: toks[0] == "true")
|
||||
| propertyPath.setParseAction(lambda toks: SubstituteVal(toks))
|
||||
) # need to add support for alg expressions
|
||||
|
||||
propertyVal = simpleVals | (lparen + Group(delimitedList(simpleVals)) + rparen)
|
||||
|
||||
boolExpression = Forward()
|
||||
boolCondition = Group(
|
||||
(Group(propertyVal)("lval") + binaryOp + Group(propertyVal)("rval")) | (lparen + boolExpression + rparen)
|
||||
)
|
||||
boolExpression << boolCondition + ZeroOrMore((and_ | or_) + boolExpression)
|
||||
|
||||
return boolExpression
|
||||
|
||||
|
||||
def double_equals_trans(lval, rval, operator):
|
||||
# double equals
|
||||
if operator in ["in", "notin"]:
|
||||
if isinstance(rval, list) and len(rval):
|
||||
transed_rval = []
|
||||
if isinstance(lval, int):
|
||||
for item in rval:
|
||||
try:
|
||||
transed_rval.append(int(item))
|
||||
except Exception:
|
||||
pass
|
||||
elif isinstance(lval, str):
|
||||
for item in rval:
|
||||
try:
|
||||
transed_rval.append(str(item))
|
||||
except Exception:
|
||||
pass
|
||||
rval += transed_rval
|
||||
|
||||
elif operator in ["issuperset", "notissuperset"]:
|
||||
# avoid convert set('abc') to {a, b, c}, but keep {'abc'}
|
||||
if isinstance(lval, str):
|
||||
lval = [lval]
|
||||
if isinstance(rval, str):
|
||||
rval = [rval]
|
||||
|
||||
else:
|
||||
try:
|
||||
if isinstance(lval, int):
|
||||
rval = int(rval)
|
||||
elif isinstance(rval, int):
|
||||
lval = int(lval)
|
||||
if isinstance(lval, str):
|
||||
rval = str(rval)
|
||||
elif isinstance(rval, str):
|
||||
lval = str(lval)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return lval, rval
|
||||
|
||||
|
||||
class BoolRule(object):
|
||||
"""
|
||||
Represents a boolean expression and provides a `test` method to evaluate
|
||||
the expression and determine its truthiness.
|
||||
|
||||
:param query: A string containing the query to be evaluated
|
||||
:param lazy: If ``True``, parse the query the first time it's tested rather
|
||||
than immediately. This can help with performance if you
|
||||
instantiate a lot of rules and only end up evaluating a
|
||||
small handful.
|
||||
"""
|
||||
|
||||
_compiled = False
|
||||
_tokens = None
|
||||
_query = None
|
||||
|
||||
def __init__(self, query, lazy=False, strict=True):
|
||||
self._query = query
|
||||
self.strict = strict
|
||||
if not lazy:
|
||||
self._compile()
|
||||
|
||||
def test(self, context=None):
|
||||
"""
|
||||
Test the expression against the given context and return the result.
|
||||
|
||||
:param context: A dict context to evaluate the expression against.
|
||||
:return: True if the expression succesfully evaluated against the
|
||||
context, or False otherwise.
|
||||
"""
|
||||
if self._is_match_all():
|
||||
return True
|
||||
|
||||
self._compile()
|
||||
return self._test_tokens(self._tokens, context)
|
||||
|
||||
def _is_match_all(self):
|
||||
return True if self._query == "*" else False
|
||||
|
||||
def _compile(self):
|
||||
if not self._compiled:
|
||||
|
||||
# special case match-all query
|
||||
if self._is_match_all():
|
||||
return
|
||||
|
||||
try:
|
||||
self._tokens = get_bool_expression().parseString(self._query, parseAll=self.strict)
|
||||
except ParseException:
|
||||
raise
|
||||
|
||||
self._compiled = True
|
||||
|
||||
def _expand_val(self, val, context):
|
||||
if type(val) == list:
|
||||
val = [self._expand_val(v, context) for v in val]
|
||||
|
||||
if isinstance(val, SubstituteVal):
|
||||
ret = val.get_val(context)
|
||||
return ret
|
||||
|
||||
if isinstance(val, ParseResults):
|
||||
return [self._expand_val(x, context) for x in val.asList()]
|
||||
|
||||
return val
|
||||
|
||||
def _test_tokens(self, tokens, context):
|
||||
passed = False
|
||||
|
||||
for token in tokens:
|
||||
|
||||
if not isinstance(token, ParseResults):
|
||||
if token == "or" and passed:
|
||||
return True
|
||||
elif token == "and" and not passed:
|
||||
return False
|
||||
continue
|
||||
|
||||
if not token.getName():
|
||||
passed = self._test_tokens(token, context)
|
||||
continue
|
||||
|
||||
items = token.asDict()
|
||||
|
||||
operator = items["operator"]
|
||||
lval = self._expand_val(items["lval"][0], context)
|
||||
rval = self._expand_val(items["rval"][0], context)
|
||||
lval, rval = double_equals_trans(lval, rval, operator)
|
||||
|
||||
if operator in ("=", "==", "eq"):
|
||||
passed = lval == rval
|
||||
elif operator in ("!=", "ne"):
|
||||
passed = lval != rval
|
||||
elif operator in (">", "gt"):
|
||||
passed = lval > rval
|
||||
elif operator in (">=", "ge"):
|
||||
passed = lval >= rval
|
||||
elif operator in ("<", "lt"):
|
||||
passed = lval < rval
|
||||
elif operator in ("<=", "le"):
|
||||
passed = lval <= rval
|
||||
elif operator == "in":
|
||||
passed = lval in rval
|
||||
elif operator == "notin":
|
||||
passed = lval not in rval
|
||||
elif operator == "issuperset":
|
||||
passed = set(lval).issuperset(set(rval))
|
||||
elif operator == "notissuperset":
|
||||
passed = not set(lval).issuperset(set(rval))
|
||||
else:
|
||||
raise UnknownOperatorException("Unknown operator '{}'".format(operator))
|
||||
|
||||
return passed
|
||||
|
||||
|
||||
class MissingVariableException(Exception):
|
||||
"""
|
||||
Raised when an expression contains a property path that's not supplied in
|
||||
the context.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class UnknownOperatorException(Exception):
|
||||
"""
|
||||
Raised when an expression uses an unknown operator.
|
||||
|
||||
This should never be thrown since the operator won't be correctly parsed as
|
||||
a token by pyparsing, but it's useful to have this hanging around for when
|
||||
additional operators are being added.
|
||||
"""
|
||||
|
||||
pass
|
||||
53
lib/bamboo_engine/utils/collections.py
Normal file
53
lib/bamboo_engine/utils/collections.py
Normal file
@@ -0,0 +1,53 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS Community
|
||||
Edition) available.
|
||||
Copyright (C) 2017-2021 THL A29 Limited, a Tencent company. All rights reserved.
|
||||
Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
http://opensource.org/licenses/MIT
|
||||
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.
|
||||
"""
|
||||
|
||||
# 集合类工具
|
||||
|
||||
|
||||
from typing import Any
|
||||
|
||||
|
||||
class FancyDict(dict):
|
||||
def __getattr__(self, key: str) -> Any:
|
||||
try:
|
||||
return self[key]
|
||||
except KeyError as k:
|
||||
raise AttributeError(k)
|
||||
|
||||
def __setattr__(self, key: str, value: Any):
|
||||
# 内建属性不放入 key 中
|
||||
if key.startswith("__") and key.endswith("__"):
|
||||
super().__setattr__(key, value)
|
||||
else:
|
||||
self[key] = value
|
||||
|
||||
def __delattr__(self, key: str):
|
||||
try:
|
||||
del self[key]
|
||||
except KeyError as k:
|
||||
raise AttributeError(k)
|
||||
|
||||
|
||||
class ConstantDict(dict):
|
||||
"""ConstantDict is a subclass of :class:`dict`, implementing __setitem__
|
||||
method to avoid item assignment::
|
||||
|
||||
>>> d = ConstantDict({'key': 'value'})
|
||||
>>> d['key'] = 'value'
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
TypeError: 'ConstantDict' object does not support item assignment
|
||||
"""
|
||||
|
||||
def __setitem__(self, key: str, value: Any):
|
||||
raise TypeError("'%s' object does not support item assignment" % self.__class__.__name__)
|
||||
22
lib/bamboo_engine/utils/constants.py
Normal file
22
lib/bamboo_engine/utils/constants.py
Normal file
@@ -0,0 +1,22 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS Community
|
||||
Edition) available.
|
||||
Copyright (C) 2017-2021 THL A29 Limited, a Tencent company. All rights reserved.
|
||||
Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
http://opensource.org/licenses/MIT
|
||||
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.
|
||||
"""
|
||||
|
||||
|
||||
from bamboo_engine.eri import ContextValueType
|
||||
|
||||
|
||||
VAR_CONTEXT_MAPPING = {
|
||||
"plain": ContextValueType.PLAIN,
|
||||
"splice": ContextValueType.SPLICE,
|
||||
"lazy": ContextValueType.COMPUTE,
|
||||
}
|
||||
261
lib/bamboo_engine/utils/graph.py
Normal file
261
lib/bamboo_engine/utils/graph.py
Normal file
@@ -0,0 +1,261 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS Community
|
||||
Edition) available.
|
||||
Copyright (C) 2017-2021 THL A29 Limited, a Tencent company. All rights reserved.
|
||||
Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
http://opensource.org/licenses/MIT
|
||||
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.
|
||||
"""
|
||||
|
||||
|
||||
class Graph(object):
|
||||
def __init__(self, nodes, flows):
|
||||
self.nodes = nodes
|
||||
self.flows = flows
|
||||
self.path = []
|
||||
self.last_visited_node = ""
|
||||
self.graph = {node: [] for node in self.nodes}
|
||||
for flow in self.flows:
|
||||
self.graph[flow[0]].append(flow[1])
|
||||
|
||||
def has_cycle(self):
|
||||
self.path = []
|
||||
visited = {node: False for node in self.nodes}
|
||||
visit_stack = {node: False for node in self.nodes}
|
||||
|
||||
for node in self.nodes:
|
||||
if self._has_cycle(node, visited, visit_stack):
|
||||
return True
|
||||
return False
|
||||
|
||||
def _has_cycle(self, node, visited, visit_stack):
|
||||
self.last_visited_node = node
|
||||
self.path.append(node)
|
||||
visited[node] = True
|
||||
visit_stack[node] = True
|
||||
|
||||
for neighbor in self.graph[node]:
|
||||
if not visited[neighbor]:
|
||||
if self._has_cycle(neighbor, visited, visit_stack):
|
||||
return True
|
||||
elif visit_stack[neighbor]:
|
||||
self.path.append(neighbor)
|
||||
return True
|
||||
|
||||
self.path.remove(node)
|
||||
visit_stack[node] = False
|
||||
return False
|
||||
|
||||
def get_cycle(self):
|
||||
if self.has_cycle():
|
||||
cross_node = self.path[-1]
|
||||
if self.path.count(cross_node) > 1:
|
||||
return self.path[self.path.index(cross_node) :]
|
||||
else:
|
||||
return self.path
|
||||
return []
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
graph1 = Graph([1, 2, 3, 4], [[1, 2], [2, 3], [3, 4]])
|
||||
assert not graph1.has_cycle()
|
||||
assert graph1.get_cycle() == []
|
||||
graph2 = Graph([1, 2, 3, 4], [[1, 2], [2, 3], [3, 4], [4, 1]])
|
||||
assert graph2.has_cycle()
|
||||
assert graph2.get_cycle() == [1, 2, 3, 4, 1]
|
||||
graph3 = Graph([1, 2, 3, 4], [[1, 2], [2, 3], [3, 4], [4, 2]])
|
||||
assert graph3.has_cycle()
|
||||
assert graph3.get_cycle() == [2, 3, 4, 2]
|
||||
graph4 = Graph(
|
||||
[
|
||||
"n20c4a0601193f268bfa168f1192eacd",
|
||||
"nef42d10350b3961b53df7af67e16d9b",
|
||||
"n0ada7b4abe63771a43052eaf188dc4b",
|
||||
"n0cd3b95c714388bacdf1a486ab432fc",
|
||||
"n1430047af8537f88710c4bbf3cbfb0f",
|
||||
"n383748fe27434d582f0ca17af9d968a",
|
||||
"n51426abd4be3a4691c80a73c3f93b3c",
|
||||
"n854753a77933562ae72ec87c365f23d",
|
||||
"n89f083892a731d7b9d7edb0f372006d",
|
||||
"n8d4568db0ad364692b0387e86a2f1e0",
|
||||
"n8daedbb02273a0fbc94cc118c90649f",
|
||||
"n90b7ef55fe839b181879e036b4f8ffe",
|
||||
"n99817348b4a36a6931854c93eed8c5f",
|
||||
"na02956eba6f3a36ab9b0af2f2350213",
|
||||
"nc3d0d49adf530bbaffe53630c184c0a",
|
||||
"nca50848d1aa340f8c2b4776ce81868d",
|
||||
"ncab9a48e79d357195dcee68dad3a31f",
|
||||
"ncb4e013a6a8348bab087cc8500a3876",
|
||||
"ne1f86f902a23e7fa4a67192e8b38a05",
|
||||
"ne26def77df1385caa206c64e7e3ea53",
|
||||
"nf3ebee137c53da28091ad7d140ce00c",
|
||||
"nfc1dcdd7476393b9a81a988c113e1cf",
|
||||
"n0197f8f210b3a1b8a7fc2f90e94744e",
|
||||
"n01fb40259ad3cf285bb11a8bbbe59f2",
|
||||
"n03f39191e8a32629145ba6a677ed040",
|
||||
"n03ffc3b9e12316d8be63261cb9dec71",
|
||||
"n07982b8985139249bca3a046f3a4379",
|
||||
"n0b9e36e6b633ddb906d2044f658f110",
|
||||
"n136c4fedebe3eb0ba932495aff6a945",
|
||||
"n17cdc62c5d43976a413bda8f35634eb",
|
||||
"n1d48483d8023439ad98d61d156c85fb",
|
||||
"n26725bdcc0931fab0bc73e7244545ca",
|
||||
"n2890db24f6c3cd1bbcd6b7d8cf2c045",
|
||||
"n2ad9caac5b737bd897d4c8844c85f12",
|
||||
"n2c88d1c1d8b35aebf883cbf259fb6bc",
|
||||
"n302d25dfc9c369ab13104d5208e7119",
|
||||
"n31688b7ab44338e9e6cb8dcaf259eef",
|
||||
"n374443fbdc1313d98ebbe19d535fec2",
|
||||
"n38c3dd0344a3f86bc7511c454bcdf4c",
|
||||
"n3934eef90463940a6a9cf4ba2e63b1c",
|
||||
"n40d5f0ca4bc3dd99c0b264cb186f00f",
|
||||
"n476ddcb6dd33e2abac43596b08c2bc1",
|
||||
"n4790f8aa48e335aa712e2af757e180b",
|
||||
"n48bbfdc912334fc89c4f48c05e8969e",
|
||||
"n5bef4f4532a382eaf79a0af70b2396b",
|
||||
"n5ced56bcc863060ac4977755f35a5f5",
|
||||
"n66a0562670e37648a3e05c243335bff",
|
||||
"n6dc118cd3f7341d9ef8c97c63e2e9d9",
|
||||
"n6e9d52e1ea53958a93e5b34022e7037",
|
||||
"n786694b5ed33295a885b5bcd8c7c1ce",
|
||||
"n7dccd56c80233469a4609f684ebe457",
|
||||
"n8492d92ab6a3da48c2b49d6fcb8a479",
|
||||
"n86a8b1a56f9399f90c4c227594a9d03",
|
||||
"n8a805c0cd02307bad9f7828880b53dc",
|
||||
"n8c7e35b0457300d9d6a96a6b1d18329",
|
||||
"n91fdaed36403d06a07f4afe85e2892c",
|
||||
"n9335d0718a937f9a39ec5b36d5637fe",
|
||||
"n9372fb07ad936cba31f3d4e440f395a",
|
||||
"n9ab96f926d83a93a5d3ebe2888fd343",
|
||||
"na2a8a54e68033d0a276eb88dbff91c3",
|
||||
"na493a7b5d5b3cc29f4070a6c4589cb7",
|
||||
"nadfa68cb2503a39aac6626d6c72484a",
|
||||
"nae1218ddd2e3448b562bc79dc084401",
|
||||
"nc012287be793377b975b0230b35d713",
|
||||
"ncb2e01f0c5336fe82b0e0e496f2612b",
|
||||
"ncb5843900903b4c8a0a8302474d8c51",
|
||||
"ncbf4db2c48f3348b2c7081f9e3b363a",
|
||||
"nd4ee6c3248935ce9239e4bb20a81ab8",
|
||||
"ndb1cf7af0e2319c9868530d0df8fd93",
|
||||
"ne36a6858a733430bffa4fec053dc1ab",
|
||||
"ne7af4a7c3613b3d81fe9e6046425a36",
|
||||
"ne8035dd8de732758c1cc623f80f2fc8",
|
||||
"ned91fdb914c35f3a21f320f62d72ffd",
|
||||
"nf5448b3c66430f4a299d08208d313a6",
|
||||
"nfaa0756a06f300495fb2e2e45e05ed3",
|
||||
],
|
||||
[
|
||||
["n8d4568db0ad364692b0387e86a2f1e0", "n5bef4f4532a382eaf79a0af70b2396b"],
|
||||
["n8daedbb02273a0fbc94cc118c90649f", "nf5448b3c66430f4a299d08208d313a6"],
|
||||
["n01fb40259ad3cf285bb11a8bbbe59f2", "ne1f86f902a23e7fa4a67192e8b38a05"],
|
||||
["ncab9a48e79d357195dcee68dad3a31f", "n0197f8f210b3a1b8a7fc2f90e94744e"],
|
||||
["na493a7b5d5b3cc29f4070a6c4589cb7", "ne1f86f902a23e7fa4a67192e8b38a05"],
|
||||
["n89f083892a731d7b9d7edb0f372006d", "n136c4fedebe3eb0ba932495aff6a945"],
|
||||
["n51426abd4be3a4691c80a73c3f93b3c", "n9ab96f926d83a93a5d3ebe2888fd343"],
|
||||
["n89f083892a731d7b9d7edb0f372006d", "n8492d92ab6a3da48c2b49d6fcb8a479"],
|
||||
["n17cdc62c5d43976a413bda8f35634eb", "n6e9d52e1ea53958a93e5b34022e7037"],
|
||||
["n476ddcb6dd33e2abac43596b08c2bc1", "ne1f86f902a23e7fa4a67192e8b38a05"],
|
||||
["n6dc118cd3f7341d9ef8c97c63e2e9d9", "nfc1dcdd7476393b9a81a988c113e1cf"],
|
||||
["n91fdaed36403d06a07f4afe85e2892c", "ncb4e013a6a8348bab087cc8500a3876"],
|
||||
["n8a805c0cd02307bad9f7828880b53dc", "n3934eef90463940a6a9cf4ba2e63b1c"],
|
||||
["n2890db24f6c3cd1bbcd6b7d8cf2c045", "n0ada7b4abe63771a43052eaf188dc4b"],
|
||||
["ned91fdb914c35f3a21f320f62d72ffd", "n383748fe27434d582f0ca17af9d968a"],
|
||||
["n89f083892a731d7b9d7edb0f372006d", "n0b9e36e6b633ddb906d2044f658f110"],
|
||||
["nc3d0d49adf530bbaffe53630c184c0a", "na493a7b5d5b3cc29f4070a6c4589cb7"],
|
||||
["ncb2e01f0c5336fe82b0e0e496f2612b", "nc012287be793377b975b0230b35d713"],
|
||||
["n86a8b1a56f9399f90c4c227594a9d03", "nf3ebee137c53da28091ad7d140ce00c"],
|
||||
["nc3d0d49adf530bbaffe53630c184c0a", "nadfa68cb2503a39aac6626d6c72484a"],
|
||||
["na02956eba6f3a36ab9b0af2f2350213", "na2a8a54e68033d0a276eb88dbff91c3"],
|
||||
["n8daedbb02273a0fbc94cc118c90649f", "n07982b8985139249bca3a046f3a4379"],
|
||||
["n136c4fedebe3eb0ba932495aff6a945", "nfc1dcdd7476393b9a81a988c113e1cf"],
|
||||
["n9372fb07ad936cba31f3d4e440f395a", "n1430047af8537f88710c4bbf3cbfb0f"],
|
||||
["n8d4568db0ad364692b0387e86a2f1e0", "n91fdaed36403d06a07f4afe85e2892c"],
|
||||
["n854753a77933562ae72ec87c365f23d", "n40d5f0ca4bc3dd99c0b264cb186f00f"],
|
||||
["n854753a77933562ae72ec87c365f23d", "n1d48483d8023439ad98d61d156c85fb"],
|
||||
["n9ab96f926d83a93a5d3ebe2888fd343", "n383748fe27434d582f0ca17af9d968a"],
|
||||
["ne36a6858a733430bffa4fec053dc1ab", "n0cd3b95c714388bacdf1a486ab432fc"],
|
||||
["n03ffc3b9e12316d8be63261cb9dec71", "nca50848d1aa340f8c2b4776ce81868d"],
|
||||
["ne8035dd8de732758c1cc623f80f2fc8", "n0ada7b4abe63771a43052eaf188dc4b"],
|
||||
["n51426abd4be3a4691c80a73c3f93b3c", "ned91fdb914c35f3a21f320f62d72ffd"],
|
||||
["nd4ee6c3248935ce9239e4bb20a81ab8", "nfaa0756a06f300495fb2e2e45e05ed3"],
|
||||
["n5bef4f4532a382eaf79a0af70b2396b", "ncb4e013a6a8348bab087cc8500a3876"],
|
||||
["ne26def77df1385caa206c64e7e3ea53", "n786694b5ed33295a885b5bcd8c7c1ce"],
|
||||
["n854753a77933562ae72ec87c365f23d", "ne8035dd8de732758c1cc623f80f2fc8"],
|
||||
["n374443fbdc1313d98ebbe19d535fec2", "ndb1cf7af0e2319c9868530d0df8fd93"],
|
||||
["nfaa0756a06f300495fb2e2e45e05ed3", "n8c7e35b0457300d9d6a96a6b1d18329"],
|
||||
["n90b7ef55fe839b181879e036b4f8ffe", "n26725bdcc0931fab0bc73e7244545ca"],
|
||||
["n8d4568db0ad364692b0387e86a2f1e0", "ncb2e01f0c5336fe82b0e0e496f2612b"],
|
||||
["ncb5843900903b4c8a0a8302474d8c51", "ncb4e013a6a8348bab087cc8500a3876"],
|
||||
["nf5448b3c66430f4a299d08208d313a6", "nf3ebee137c53da28091ad7d140ce00c"],
|
||||
["n20c4a0601193f268bfa168f1192eacd", "nd4ee6c3248935ce9239e4bb20a81ab8"],
|
||||
["nca50848d1aa340f8c2b4776ce81868d", "nc3d0d49adf530bbaffe53630c184c0a"],
|
||||
["na02956eba6f3a36ab9b0af2f2350213", "n03ffc3b9e12316d8be63261cb9dec71"],
|
||||
["n7dccd56c80233469a4609f684ebe457", "n8daedbb02273a0fbc94cc118c90649f"],
|
||||
["n0ada7b4abe63771a43052eaf188dc4b", "na02956eba6f3a36ab9b0af2f2350213"],
|
||||
["n9335d0718a937f9a39ec5b36d5637fe", "n99817348b4a36a6931854c93eed8c5f"],
|
||||
["n90b7ef55fe839b181879e036b4f8ffe", "n5ced56bcc863060ac4977755f35a5f5"],
|
||||
["ncb4e013a6a8348bab087cc8500a3876", "ne26def77df1385caa206c64e7e3ea53"],
|
||||
["na02956eba6f3a36ab9b0af2f2350213", "n4790f8aa48e335aa712e2af757e180b"],
|
||||
["nc012287be793377b975b0230b35d713", "ncb4e013a6a8348bab087cc8500a3876"],
|
||||
["n8d4568db0ad364692b0387e86a2f1e0", "ncb5843900903b4c8a0a8302474d8c51"],
|
||||
["n40d5f0ca4bc3dd99c0b264cb186f00f", "n0ada7b4abe63771a43052eaf188dc4b"],
|
||||
["n38c3dd0344a3f86bc7511c454bcdf4c", "n17cdc62c5d43976a413bda8f35634eb"],
|
||||
["n6e9d52e1ea53958a93e5b34022e7037", "n90b7ef55fe839b181879e036b4f8ffe"],
|
||||
["nf3ebee137c53da28091ad7d140ce00c", "n51426abd4be3a4691c80a73c3f93b3c"],
|
||||
["n99817348b4a36a6931854c93eed8c5f", "n89f083892a731d7b9d7edb0f372006d"],
|
||||
["n89f083892a731d7b9d7edb0f372006d", "n6dc118cd3f7341d9ef8c97c63e2e9d9"],
|
||||
["n8daedbb02273a0fbc94cc118c90649f", "n66a0562670e37648a3e05c243335bff"],
|
||||
["nadfa68cb2503a39aac6626d6c72484a", "ne1f86f902a23e7fa4a67192e8b38a05"],
|
||||
["n383748fe27434d582f0ca17af9d968a", "nef42d10350b3961b53df7af67e16d9b"],
|
||||
["na02956eba6f3a36ab9b0af2f2350213", "n03f39191e8a32629145ba6a677ed040"],
|
||||
["nae1218ddd2e3448b562bc79dc084401", "n383748fe27434d582f0ca17af9d968a"],
|
||||
["n26725bdcc0931fab0bc73e7244545ca", "n1430047af8537f88710c4bbf3cbfb0f"],
|
||||
["n48bbfdc912334fc89c4f48c05e8969e", "n8a805c0cd02307bad9f7828880b53dc"],
|
||||
["ne7af4a7c3613b3d81fe9e6046425a36", "ncb4e013a6a8348bab087cc8500a3876"],
|
||||
["nfc1dcdd7476393b9a81a988c113e1cf", "n8d4568db0ad364692b0387e86a2f1e0"],
|
||||
["n0197f8f210b3a1b8a7fc2f90e94744e", "n99817348b4a36a6931854c93eed8c5f"],
|
||||
["n90b7ef55fe839b181879e036b4f8ffe", "n302d25dfc9c369ab13104d5208e7119"],
|
||||
["n1d48483d8023439ad98d61d156c85fb", "n0ada7b4abe63771a43052eaf188dc4b"],
|
||||
["na2a8a54e68033d0a276eb88dbff91c3", "nca50848d1aa340f8c2b4776ce81868d"],
|
||||
["n90b7ef55fe839b181879e036b4f8ffe", "n9372fb07ad936cba31f3d4e440f395a"],
|
||||
["ndb1cf7af0e2319c9868530d0df8fd93", "n2ad9caac5b737bd897d4c8844c85f12"],
|
||||
["n8492d92ab6a3da48c2b49d6fcb8a479", "nfc1dcdd7476393b9a81a988c113e1cf"],
|
||||
["n8d4568db0ad364692b0387e86a2f1e0", "ne7af4a7c3613b3d81fe9e6046425a36"],
|
||||
["n302d25dfc9c369ab13104d5208e7119", "n1430047af8537f88710c4bbf3cbfb0f"],
|
||||
["n51426abd4be3a4691c80a73c3f93b3c", "n2c88d1c1d8b35aebf883cbf259fb6bc"],
|
||||
["n786694b5ed33295a885b5bcd8c7c1ce", "n0cd3b95c714388bacdf1a486ab432fc"],
|
||||
["n854753a77933562ae72ec87c365f23d", "n2890db24f6c3cd1bbcd6b7d8cf2c045"],
|
||||
["nc3d0d49adf530bbaffe53630c184c0a", "n476ddcb6dd33e2abac43596b08c2bc1"],
|
||||
["n2c88d1c1d8b35aebf883cbf259fb6bc", "n383748fe27434d582f0ca17af9d968a"],
|
||||
["n0cd3b95c714388bacdf1a486ab432fc", "n854753a77933562ae72ec87c365f23d"],
|
||||
["n51426abd4be3a4691c80a73c3f93b3c", "nae1218ddd2e3448b562bc79dc084401"],
|
||||
["nc3d0d49adf530bbaffe53630c184c0a", "n01fb40259ad3cf285bb11a8bbbe59f2"],
|
||||
["ne1f86f902a23e7fa4a67192e8b38a05", "n374443fbdc1313d98ebbe19d535fec2"],
|
||||
["n0b9e36e6b633ddb906d2044f658f110", "nfc1dcdd7476393b9a81a988c113e1cf"],
|
||||
["ncab9a48e79d357195dcee68dad3a31f", "ncbf4db2c48f3348b2c7081f9e3b363a"],
|
||||
["n8daedbb02273a0fbc94cc118c90649f", "n86a8b1a56f9399f90c4c227594a9d03"],
|
||||
["ncbf4db2c48f3348b2c7081f9e3b363a", "n99817348b4a36a6931854c93eed8c5f"],
|
||||
["n1430047af8537f88710c4bbf3cbfb0f", "ncab9a48e79d357195dcee68dad3a31f"],
|
||||
["n4790f8aa48e335aa712e2af757e180b", "nca50848d1aa340f8c2b4776ce81868d"],
|
||||
["ne26def77df1385caa206c64e7e3ea53", "ne36a6858a733430bffa4fec053dc1ab"],
|
||||
["ncab9a48e79d357195dcee68dad3a31f", "n31688b7ab44338e9e6cb8dcaf259eef"],
|
||||
["n07982b8985139249bca3a046f3a4379", "nf3ebee137c53da28091ad7d140ce00c"],
|
||||
["n66a0562670e37648a3e05c243335bff", "nf3ebee137c53da28091ad7d140ce00c"],
|
||||
["n03f39191e8a32629145ba6a677ed040", "nca50848d1aa340f8c2b4776ce81868d"],
|
||||
["n8c7e35b0457300d9d6a96a6b1d18329", "n38c3dd0344a3f86bc7511c454bcdf4c"],
|
||||
["n5ced56bcc863060ac4977755f35a5f5", "n1430047af8537f88710c4bbf3cbfb0f"],
|
||||
["n2ad9caac5b737bd897d4c8844c85f12", "n48bbfdc912334fc89c4f48c05e8969e"],
|
||||
["n31688b7ab44338e9e6cb8dcaf259eef", "n99817348b4a36a6931854c93eed8c5f"],
|
||||
["n3934eef90463940a6a9cf4ba2e63b1c", "n7dccd56c80233469a4609f684ebe457"],
|
||||
["ncab9a48e79d357195dcee68dad3a31f", "n9335d0718a937f9a39ec5b36d5637fe"],
|
||||
],
|
||||
)
|
||||
assert not graph4.has_cycle()
|
||||
assert graph4.get_cycle() == []
|
||||
graph5 = Graph([1, 2, 3, 4, 5], [[1, 2], [2, 3], [2, 4], [4, 5], [5, 2]])
|
||||
assert graph5.has_cycle()
|
||||
assert graph5.get_cycle() == [2, 4, 5, 2]
|
||||
21
lib/bamboo_engine/utils/host.py
Normal file
21
lib/bamboo_engine/utils/host.py
Normal file
@@ -0,0 +1,21 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS Community
|
||||
Edition) available.
|
||||
Copyright (C) 2017-2021 THL A29 Limited, a Tencent company. All rights reserved.
|
||||
Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
http://opensource.org/licenses/MIT
|
||||
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 socket
|
||||
|
||||
|
||||
def get_hostname():
|
||||
"""
|
||||
获取当前主机名
|
||||
"""
|
||||
return socket.gethostname()
|
||||
55
lib/bamboo_engine/utils/mako_safety.py
Normal file
55
lib/bamboo_engine/utils/mako_safety.py
Normal file
@@ -0,0 +1,55 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS Community
|
||||
Edition) available.
|
||||
Copyright (C) 2017-2021 THL A29 Limited, a Tencent company. All rights reserved.
|
||||
Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
http://opensource.org/licenses/MIT
|
||||
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.
|
||||
"""
|
||||
|
||||
# Mako 安全工具
|
||||
|
||||
|
||||
from ast import NodeVisitor
|
||||
|
||||
from mako import parsetree
|
||||
|
||||
from .mako_utils.code_extract import MakoNodeCodeExtractor
|
||||
from .mako_utils.exceptions import ForbiddenMakoTemplateException
|
||||
|
||||
|
||||
class SingleLineNodeVisitor(NodeVisitor):
|
||||
"""
|
||||
遍历语法树节点,遇到魔术方法使用或 import 时,抛出异常
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(SingleLineNodeVisitor, self).__init__(*args, **kwargs)
|
||||
|
||||
def visit_Attribute(self, node):
|
||||
if node.attr.startswith("__"):
|
||||
raise ForbiddenMakoTemplateException("can not access private attribute")
|
||||
|
||||
def visit_Name(self, node):
|
||||
if node.id.startswith("__"):
|
||||
raise ForbiddenMakoTemplateException("can not access private method")
|
||||
|
||||
def visit_Import(self, node):
|
||||
raise ForbiddenMakoTemplateException("can not use import statement")
|
||||
|
||||
def visit_ImportFrom(self, node):
|
||||
self.visit_Import(node)
|
||||
|
||||
|
||||
class SingleLinCodeExtractor(MakoNodeCodeExtractor):
|
||||
def extract(self, node):
|
||||
if isinstance(node, parsetree.Code) or isinstance(node, parsetree.Expression):
|
||||
return node.text
|
||||
elif isinstance(node, parsetree.Text):
|
||||
return None
|
||||
else:
|
||||
raise ForbiddenMakoTemplateException("Unsupported node: [{}]".format(node.__class__.__name__))
|
||||
16
lib/bamboo_engine/utils/mako_utils/__init__.py
Normal file
16
lib/bamboo_engine/utils/mako_utils/__init__.py
Normal file
@@ -0,0 +1,16 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS Community
|
||||
Edition) available.
|
||||
Copyright (C) 2017-2021 THL A29 Limited, a Tencent company. All rights reserved.
|
||||
Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
http://opensource.org/licenses/MIT
|
||||
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.
|
||||
"""
|
||||
|
||||
"""
|
||||
Mako 相关工具模块
|
||||
"""
|
||||
59
lib/bamboo_engine/utils/mako_utils/checker.py
Normal file
59
lib/bamboo_engine/utils/mako_utils/checker.py
Normal file
@@ -0,0 +1,59 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS Community
|
||||
Edition) available.
|
||||
Copyright (C) 2017-2021 THL A29 Limited, a Tencent company. All rights reserved.
|
||||
Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
http://opensource.org/licenses/MIT
|
||||
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 ast
|
||||
from typing import List
|
||||
|
||||
from mako import parsetree
|
||||
from mako.exceptions import MakoException
|
||||
from mako.lexer import Lexer
|
||||
|
||||
from .code_extract import MakoNodeCodeExtractor
|
||||
from .exceptions import ForbiddenMakoTemplateException
|
||||
|
||||
|
||||
def parse_template_nodes(
|
||||
nodes: List[parsetree.Node],
|
||||
node_visitor: ast.NodeVisitor,
|
||||
code_extractor: MakoNodeCodeExtractor,
|
||||
):
|
||||
"""
|
||||
解析mako模板节点,逐个节点解析抽象语法树并检查安全性
|
||||
:param nodes: mako模板节点列表
|
||||
:param node_visitor: 节点访问类,用于遍历AST节点
|
||||
:param code_extractor: Mako 词法节点处理器,用于提取 python 代码
|
||||
"""
|
||||
for node in nodes:
|
||||
code = code_extractor.extract(node)
|
||||
if code is None:
|
||||
continue
|
||||
|
||||
ast_node = ast.parse(code, "<unknown>", "exec")
|
||||
node_visitor.visit(ast_node)
|
||||
if hasattr(node, "nodes"):
|
||||
parse_template_nodes(node.nodes, node_visitor)
|
||||
|
||||
|
||||
def check_mako_template_safety(text: str, node_visitor: ast.NodeVisitor, code_extractor: MakoNodeCodeExtractor) -> bool:
|
||||
"""
|
||||
检查mako模板是否安全,若不安全直接抛出异常,安全则返回True
|
||||
:param text: mako模板内容
|
||||
:param node_visitor: 节点访问器,用于遍历AST节点
|
||||
"""
|
||||
try:
|
||||
lexer_template = Lexer(text).parse()
|
||||
except MakoException as mako_error:
|
||||
raise ForbiddenMakoTemplateException("非mako模板,解析失败, {err_msg}".format(err_msg=mako_error.__class__.__name__))
|
||||
parse_template_nodes(lexer_template.nodes, node_visitor, code_extractor)
|
||||
return True
|
||||
45
lib/bamboo_engine/utils/mako_utils/code_extract.py
Normal file
45
lib/bamboo_engine/utils/mako_utils/code_extract.py
Normal file
@@ -0,0 +1,45 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS Community
|
||||
Edition) available.
|
||||
Copyright (C) 2017-2021 THL A29 Limited, a Tencent company. All rights reserved.
|
||||
Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
http://opensource.org/licenses/MIT
|
||||
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 abc
|
||||
|
||||
from mako import parsetree
|
||||
from mako.ast import PythonFragment
|
||||
|
||||
from .exceptions import ForbiddenMakoTemplateException
|
||||
|
||||
|
||||
class MakoNodeCodeExtractor:
|
||||
@abc.abstractmethod
|
||||
def extract(self, node):
|
||||
"""
|
||||
处理 Mako Lexer 分割出来的 code 对象,返回需要检测的 python 代码,返回 None 表示该节点不需要处理
|
||||
|
||||
:param node: mako parsetree node
|
||||
:return: 需要处理的代码,或 None
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
class StrictMakoNodeCodeExtractor(MakoNodeCodeExtractor):
|
||||
def extract(self, node):
|
||||
if isinstance(node, parsetree.Code) or isinstance(node, parsetree.Expression):
|
||||
return node.text
|
||||
elif isinstance(node, parsetree.ControlLine):
|
||||
if node.isend:
|
||||
return None
|
||||
return PythonFragment(node.text).code
|
||||
elif isinstance(node, parsetree.Text):
|
||||
return None
|
||||
else:
|
||||
raise ForbiddenMakoTemplateException("不支持[{}]节点".format(node.__class__.__name__))
|
||||
16
lib/bamboo_engine/utils/mako_utils/exceptions.py
Normal file
16
lib/bamboo_engine/utils/mako_utils/exceptions.py
Normal file
@@ -0,0 +1,16 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS Community
|
||||
Edition) available.
|
||||
Copyright (C) 2017-2021 THL A29 Limited, a Tencent company. All rights reserved.
|
||||
Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
http://opensource.org/licenses/MIT
|
||||
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.
|
||||
"""
|
||||
|
||||
|
||||
class ForbiddenMakoTemplateException(Exception):
|
||||
pass
|
||||
115
lib/bamboo_engine/utils/mako_utils/visitors.py
Normal file
115
lib/bamboo_engine/utils/mako_utils/visitors.py
Normal file
@@ -0,0 +1,115 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS Community
|
||||
Edition) available.
|
||||
Copyright (C) 2017-2021 THL A29 Limited, a Tencent company. All rights reserved.
|
||||
Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
http://opensource.org/licenses/MIT
|
||||
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 _ast
|
||||
import ast
|
||||
|
||||
from werkzeug.utils import import_string
|
||||
|
||||
from .exceptions import ForbiddenMakoTemplateException
|
||||
|
||||
|
||||
class StrictNodeVisitor(ast.NodeVisitor):
|
||||
"""
|
||||
遍历语法树节点,遇到魔术方法使用或import时,抛出异常
|
||||
"""
|
||||
|
||||
BLACK_LIST_MODULE_METHODS = {
|
||||
"os": dir(__import__("os")),
|
||||
"subprocess": dir(__import__("subprocess")),
|
||||
"shutil": dir(__import__("shutil")),
|
||||
"ctypes": dir(__import__("ctypes")),
|
||||
"codecs": dir(__import__("codecs")),
|
||||
"sys": dir(__import__("sys")),
|
||||
"socket": dir(__import__("socket")),
|
||||
"webbrowser": dir(__import__("webbrowser")),
|
||||
"threading": dir(__import__("threading")),
|
||||
"sqlite3": dir(__import__("threading")),
|
||||
"signal": dir(__import__("signal")),
|
||||
"imaplib": dir(__import__("imaplib")),
|
||||
"fcntl": dir(__import__("fcntl")),
|
||||
"pdb": dir(__import__("pdb")),
|
||||
"pty": dir(__import__("pty")),
|
||||
"glob": dir(__import__("glob")),
|
||||
"tempfile": dir(__import__("tempfile")),
|
||||
"types": dir(import_string("types.CodeType")) + dir(import_string("types.FrameType")),
|
||||
"builtins": [
|
||||
"getattr",
|
||||
"hasattr",
|
||||
"breakpoint",
|
||||
"compile",
|
||||
"delattr",
|
||||
"open",
|
||||
"eval",
|
||||
"exec",
|
||||
"execfile",
|
||||
"exit",
|
||||
"dir",
|
||||
"globals",
|
||||
"locals",
|
||||
"input",
|
||||
"iter",
|
||||
"next",
|
||||
"quit",
|
||||
"setattr",
|
||||
"vars",
|
||||
"memoryview",
|
||||
"super",
|
||||
"print",
|
||||
],
|
||||
}
|
||||
|
||||
BLACK_LIST_METHODS = []
|
||||
for module_name, methods in BLACK_LIST_MODULE_METHODS.items():
|
||||
BLACK_LIST_METHODS.append(module_name)
|
||||
BLACK_LIST_METHODS.extend(methods)
|
||||
BLACK_LIST_METHODS = set(BLACK_LIST_METHODS)
|
||||
|
||||
WHITE_LIST_MODULES = ["datetime", "re", "random", "json", "math"]
|
||||
|
||||
def __init__(self, black_list_methods=None, white_list_modules=None):
|
||||
self.black_list_methods = black_list_methods or self.BLACK_LIST_METHODS
|
||||
self.white_list_modules = white_list_modules or self.WHITE_LIST_MODULES
|
||||
|
||||
@staticmethod
|
||||
def is_white_list_ast_obj(ast_obj: _ast.AST) -> bool:
|
||||
"""
|
||||
判断是否白名单对象,特殊豁免
|
||||
:param ast_obj: 抽象语法树节点
|
||||
:return: bool
|
||||
"""
|
||||
# re 正则表达式允许使用 compile
|
||||
if isinstance(ast_obj, _ast.Attribute) and isinstance(ast_obj.value, _ast.Name):
|
||||
if ast_obj.value.id == "re" and ast_obj.attr in ["compile"]:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def visit_Attribute(self, node):
|
||||
if self.is_white_list_ast_obj(node):
|
||||
return
|
||||
|
||||
if node.attr in self.black_list_methods or node.attr.startswith("_"):
|
||||
raise ForbiddenMakoTemplateException("Mako template forbidden.")
|
||||
|
||||
def visit_Name(self, node):
|
||||
if node.id in self.black_list_methods or node.id.startswith("_"):
|
||||
raise ForbiddenMakoTemplateException("Mako template forbidden.")
|
||||
|
||||
def visit_Import(self, node):
|
||||
for name in node.names:
|
||||
if name.name not in self.white_list_modules:
|
||||
raise ForbiddenMakoTemplateException("Mako template forbidden.")
|
||||
|
||||
def visit_ImportFrom(self, node):
|
||||
self.visit_Import(node)
|
||||
19
lib/bamboo_engine/utils/object.py
Normal file
19
lib/bamboo_engine/utils/object.py
Normal file
@@ -0,0 +1,19 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS Community
|
||||
Edition) available.
|
||||
Copyright (C) 2017-2021 THL A29 Limited, a Tencent company. All rights reserved.
|
||||
Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
http://opensource.org/licenses/MIT
|
||||
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.
|
||||
"""
|
||||
|
||||
|
||||
class Representable:
|
||||
def __str__(self):
|
||||
return "<%s: %s>" % (self.__class__.__name__, self.__dict__)
|
||||
|
||||
__repr__ = __str__
|
||||
75
lib/bamboo_engine/utils/string.py
Normal file
75
lib/bamboo_engine/utils/string.py
Normal file
@@ -0,0 +1,75 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS Community
|
||||
Edition) available.
|
||||
Copyright (C) 2017-2021 THL A29 Limited, a Tencent company. All rights reserved.
|
||||
Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
http://opensource.org/licenses/MIT
|
||||
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 uuid
|
||||
|
||||
ESCAPED_CHARS = {"\n": r"\n", "\r": r"\r", "\t": r"\t"}
|
||||
|
||||
|
||||
def transform_escape_char(string: str) -> str:
|
||||
"""
|
||||
对未转义的字符串进行转义,现有的转义字符包括\n, \r, \t
|
||||
"""
|
||||
if not isinstance(string, str):
|
||||
return string
|
||||
# 已转义的情况
|
||||
if len([c for c in ESCAPED_CHARS.values() if c in string]) > 0:
|
||||
return string
|
||||
for key, value in ESCAPED_CHARS.items():
|
||||
if key in string:
|
||||
string = string.replace(key, value)
|
||||
return string
|
||||
|
||||
|
||||
def format_var_key(key: str) -> str:
|
||||
"""
|
||||
format key to ${key}
|
||||
|
||||
:param key: key
|
||||
:type key: str
|
||||
:return: format key
|
||||
:rtype: str
|
||||
"""
|
||||
return "${%s}" % key
|
||||
|
||||
|
||||
def deformat_var_key(key: str) -> str:
|
||||
"""
|
||||
deformat ${key} to key
|
||||
|
||||
:param key: key
|
||||
:type key: str
|
||||
:return: deformat key
|
||||
:rtype: str
|
||||
"""
|
||||
return key[2:-1]
|
||||
|
||||
|
||||
def unique_id(prefix: str) -> str:
|
||||
if len(prefix) != 1:
|
||||
raise ValueError("prefix length must be 1")
|
||||
|
||||
return "{}{}".format(prefix, uuid.uuid4().hex)
|
||||
|
||||
|
||||
def get_lower_case_name(text: str) -> str:
|
||||
lst = []
|
||||
for index, char in enumerate(text):
|
||||
if char.isupper() and index != 0:
|
||||
lst.append("_")
|
||||
lst.append(char)
|
||||
|
||||
return "".join(lst).lower()
|
||||
14
lib/bamboo_engine/validator/__init__.py
Normal file
14
lib/bamboo_engine/validator/__init__.py
Normal file
@@ -0,0 +1,14 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS Community
|
||||
Edition) available.
|
||||
Copyright (C) 2017-2021 THL A29 Limited, a Tencent company. All rights reserved.
|
||||
Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
http://opensource.org/licenses/MIT
|
||||
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.
|
||||
"""
|
||||
|
||||
from .api import validate_and_process_pipeline # noqa
|
||||
49
lib/bamboo_engine/validator/api.py
Normal file
49
lib/bamboo_engine/validator/api.py
Normal file
@@ -0,0 +1,49 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS Community
|
||||
Edition) available.
|
||||
Copyright (C) 2017-2021 THL A29 Limited, a Tencent company. All rights reserved.
|
||||
Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
http://opensource.org/licenses/MIT
|
||||
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.
|
||||
"""
|
||||
|
||||
from bamboo_engine.eri import NodeType
|
||||
from bamboo_engine import exceptions
|
||||
|
||||
from . import rules
|
||||
from .connection import (
|
||||
validate_graph_connection,
|
||||
validate_graph_without_circle,
|
||||
)
|
||||
from .gateway import validate_gateways, validate_stream
|
||||
from .utils import format_pipeline_tree_io_to_list
|
||||
|
||||
|
||||
def validate_and_process_pipeline(pipeline: dict, cycle_tolerate=False):
|
||||
for subproc in [act for act in pipeline["activities"].values() if act["type"] == NodeType.SubProcess.value]:
|
||||
validate_and_process_pipeline(subproc["pipeline"], cycle_tolerate)
|
||||
|
||||
format_pipeline_tree_io_to_list(pipeline)
|
||||
# 1. connection validation
|
||||
validate_graph_connection(pipeline)
|
||||
|
||||
# do not tolerate circle in flow
|
||||
if not cycle_tolerate:
|
||||
no_cycle = validate_graph_without_circle(pipeline)
|
||||
if not no_cycle["result"]:
|
||||
raise exceptions.TreeInvalidException(no_cycle["message"])
|
||||
|
||||
# 2. gateway validation
|
||||
validate_gateways(pipeline)
|
||||
|
||||
# 3. stream validation
|
||||
validate_stream(pipeline)
|
||||
|
||||
|
||||
def add_sink_type(node_type: str):
|
||||
rules.FLOW_NODES_WITHOUT_STARTEVENT.append(node_type)
|
||||
rules.NODE_RULES[node_type] = rules.SINK_RULE
|
||||
69
lib/bamboo_engine/validator/connection.py
Normal file
69
lib/bamboo_engine/validator/connection.py
Normal file
@@ -0,0 +1,69 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS Community
|
||||
Edition) available.
|
||||
Copyright (C) 2017-2021 THL A29 Limited, a Tencent company. All rights reserved.
|
||||
Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
http://opensource.org/licenses/MIT
|
||||
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.
|
||||
"""
|
||||
|
||||
from bamboo_engine.utils.graph import Graph
|
||||
from bamboo_engine.exceptions import ConnectionValidateError
|
||||
|
||||
from .rules import NODE_RULES
|
||||
from .utils import get_nodes_dict
|
||||
|
||||
|
||||
def validate_graph_connection(data):
|
||||
"""
|
||||
节点连接合法性校验
|
||||
"""
|
||||
nodes = get_nodes_dict(data)
|
||||
|
||||
result = {"result": True, "message": {}, "failed_nodes": []}
|
||||
|
||||
for i in nodes:
|
||||
node_type = nodes[i]["type"]
|
||||
rule = NODE_RULES[node_type]
|
||||
message = ""
|
||||
for j in nodes[i]["target"]:
|
||||
if nodes[j]["type"] not in rule["allowed_out"]:
|
||||
message += "不能连接%s类型节点\n" % nodes[i]["type"]
|
||||
if rule["min_in"] > len(nodes[i]["source"]) or len(nodes[i]["source"]) > rule["max_in"]:
|
||||
message += "节点的入度最大为%s,最小为%s\n" % (rule["max_in"], rule["min_in"])
|
||||
if rule["min_out"] > len(nodes[i]["target"]) or len(nodes[i]["target"]) > rule["max_out"]:
|
||||
message += "节点的出度最大为%s,最小为%s\n" % (rule["max_out"], rule["min_out"])
|
||||
if message:
|
||||
result["failed_nodes"].append(i)
|
||||
result["message"][i] = message
|
||||
|
||||
if result["failed_nodes"]:
|
||||
raise ConnectionValidateError(failed_nodes=result["failed_nodes"], detail=result["message"])
|
||||
|
||||
|
||||
def validate_graph_without_circle(data):
|
||||
"""
|
||||
validate if a graph has not cycle
|
||||
|
||||
return {
|
||||
"result": False,
|
||||
"message": "error message",
|
||||
"error_data": ["node1_id", "node2_id", "node1_id"]
|
||||
}
|
||||
"""
|
||||
|
||||
nodes = [data["start_event"]["id"], data["end_event"]["id"]]
|
||||
nodes += list(data["gateways"].keys()) + list(data["activities"].keys())
|
||||
flows = [[flow["source"], flow["target"]] for _, flow in list(data["flows"].items())]
|
||||
cycle = Graph(nodes, flows).get_cycle()
|
||||
if cycle:
|
||||
return {
|
||||
"result": False,
|
||||
"message": "pipeline graph has circle",
|
||||
"error_data": cycle,
|
||||
}
|
||||
return {"result": True, "data": []}
|
||||
506
lib/bamboo_engine/validator/gateway.py
Normal file
506
lib/bamboo_engine/validator/gateway.py
Normal file
@@ -0,0 +1,506 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS Community
|
||||
Edition) available.
|
||||
Copyright (C) 2017-2021 THL A29 Limited, a Tencent company. All rights reserved.
|
||||
Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
http://opensource.org/licenses/MIT
|
||||
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 queue
|
||||
|
||||
from bamboo_engine import exceptions
|
||||
from .utils import get_node_for_sequence, get_nodes_dict
|
||||
|
||||
STREAM = "stream"
|
||||
P_STREAM = "p_stream"
|
||||
P = "p"
|
||||
MAIN_STREAM = "main"
|
||||
|
||||
PARALLEL_GATEWAYS = {"ParallelGateway", "ConditionalParallelGateway"}
|
||||
|
||||
|
||||
def not_in_parallel_gateway(gateway_stack, start_from=None):
|
||||
"""
|
||||
check whether there is parallel gateway in stack from specific gateway
|
||||
:param gateway_stack:
|
||||
:param start_from:
|
||||
:return:
|
||||
"""
|
||||
start = 0
|
||||
if start_from:
|
||||
id_stack = [g["id"] for g in gateway_stack]
|
||||
start = id_stack.index(start_from)
|
||||
|
||||
for i in range(start, len(gateway_stack)):
|
||||
gateway = gateway_stack[i]
|
||||
if gateway["type"] in PARALLEL_GATEWAYS:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def matched_in_prev_blocks(gid, current_start, block_nodes):
|
||||
"""
|
||||
check whether gateway with gid is matched in previous block
|
||||
:param gid:
|
||||
:param current_start:
|
||||
:param block_nodes:
|
||||
:return:
|
||||
"""
|
||||
prev_nodes = set()
|
||||
for prev_start, nodes in list(block_nodes.items()):
|
||||
if prev_start == current_start:
|
||||
continue
|
||||
prev_nodes.update(nodes)
|
||||
|
||||
return gid in prev_nodes
|
||||
|
||||
|
||||
def match_converge(
|
||||
converges,
|
||||
gateways,
|
||||
cur_index,
|
||||
end_event_id,
|
||||
block_start,
|
||||
block_nodes,
|
||||
converged,
|
||||
dist_from_start,
|
||||
converge_in_len,
|
||||
stack=None,
|
||||
):
|
||||
"""
|
||||
find converge for parallel and exclusive in blocks, and check sanity of gateway
|
||||
:param converges:
|
||||
:param gateways:
|
||||
:param cur_index:
|
||||
:param end_event_id:
|
||||
:param block_start:
|
||||
:param block_nodes:
|
||||
:param converged:
|
||||
:param dist_from_start:
|
||||
:param stack:
|
||||
:param converge_in_len:
|
||||
:return:
|
||||
"""
|
||||
|
||||
if stack is None:
|
||||
stack = []
|
||||
|
||||
if cur_index not in gateways:
|
||||
return None, False
|
||||
|
||||
# return if this node is already matched
|
||||
if gateways[cur_index]["match"]:
|
||||
return gateways[cur_index]["match"], gateways[cur_index]["share_converge"]
|
||||
|
||||
current_gateway = gateways[cur_index]
|
||||
target = gateways[cur_index]["target"]
|
||||
stack.append(gateways[cur_index])
|
||||
stack_id_set = {g["id"] for g in stack}
|
||||
|
||||
# find closest converge recursively
|
||||
for i in range(len(target)):
|
||||
|
||||
# do not process prev blocks nodes
|
||||
if matched_in_prev_blocks(target[i], block_start, block_nodes):
|
||||
target[i] = None
|
||||
continue
|
||||
|
||||
block_nodes[block_start].add(target[i])
|
||||
|
||||
# do not find self's converge node again
|
||||
while target[i] in gateways and target[i] != current_gateway["id"]:
|
||||
|
||||
if target[i] in stack_id_set:
|
||||
# return to previous gateway
|
||||
|
||||
if not_in_parallel_gateway(stack, start_from=target[i]):
|
||||
# do not trace back
|
||||
target[i] = None
|
||||
break
|
||||
else:
|
||||
raise exceptions.ConvergeMatchError(cur_index, "并行网关中的分支网关必须将所有分支汇聚到一个汇聚网关")
|
||||
|
||||
converge_id, shared = match_converge(
|
||||
converges=converges,
|
||||
gateways=gateways,
|
||||
cur_index=target[i],
|
||||
end_event_id=end_event_id,
|
||||
block_start=block_start,
|
||||
block_nodes=block_nodes,
|
||||
stack=stack,
|
||||
converged=converged,
|
||||
dist_from_start=dist_from_start,
|
||||
converge_in_len=converge_in_len,
|
||||
)
|
||||
if converge_id:
|
||||
target[i] = converge_id
|
||||
|
||||
if not shared:
|
||||
# try to get next node fo converge which is not shared
|
||||
target[i] = converges[converge_id]["target"][0]
|
||||
|
||||
else:
|
||||
# can't find corresponding converge gateway, which means this gateway will reach end event directly
|
||||
target[i] = end_event_id
|
||||
|
||||
if target[i] in converges and dist_from_start[target[i]] < dist_from_start[cur_index]:
|
||||
# do not match previous converge
|
||||
target[i] = None
|
||||
|
||||
stack.pop()
|
||||
|
||||
is_exg = current_gateway["type"] == "ExclusiveGateway"
|
||||
converge_id = None
|
||||
shared = False
|
||||
cur_to_converge = len(target)
|
||||
converge_end = False
|
||||
|
||||
# gateway match validation
|
||||
for i in range(len(target)):
|
||||
|
||||
# mark first converge
|
||||
if target[i] in converges and not converge_id:
|
||||
converge_id = target[i]
|
||||
|
||||
# same converge node
|
||||
elif target[i] in converges and converge_id == target[i]:
|
||||
pass
|
||||
|
||||
# exclusive gateway point to end
|
||||
elif is_exg and target[i] == end_event_id:
|
||||
if not_in_parallel_gateway(stack):
|
||||
converge_end = True
|
||||
else:
|
||||
raise exceptions.ConvergeMatchError(cur_index, "并行网关中的分支网关必须将所有分支汇聚到一个汇聚网关")
|
||||
|
||||
# exclusive gateway point back to self
|
||||
elif is_exg and target[i] == current_gateway["id"]:
|
||||
# not converge behavior
|
||||
cur_to_converge -= 1
|
||||
pass
|
||||
|
||||
# exclusive gateway converge at different converge gateway
|
||||
elif is_exg and target[i] in converges and converge_id != target[i]:
|
||||
raise exceptions.ConvergeMatchError(cur_index, "分支网关的所有分支第一个遇到的汇聚网关必须是同一个")
|
||||
|
||||
# meet previous node
|
||||
elif is_exg and target[i] is None:
|
||||
# not converge behavior
|
||||
cur_to_converge -= 1
|
||||
pass
|
||||
|
||||
# invalid cases
|
||||
else:
|
||||
raise exceptions.ConvergeMatchError(cur_index, "非法网关,请检查其分支是否符合规则")
|
||||
|
||||
if is_exg:
|
||||
if converge_id in converges:
|
||||
# this converge is shared by multiple gateway
|
||||
# only compare to the number of positive incoming
|
||||
shared = converge_in_len[converge_id] > cur_to_converge or converge_id in converged
|
||||
else:
|
||||
# for parallel gateway
|
||||
|
||||
converge_incoming = len(converges[converge_id]["incoming"])
|
||||
gateway_outgoing = len(target)
|
||||
|
||||
if converge_incoming > gateway_outgoing:
|
||||
for gateway_id in converged.get(converge_id, []):
|
||||
# find another parallel gateway
|
||||
if gateways[gateway_id]["type"] in PARALLEL_GATEWAYS:
|
||||
raise exceptions.ConvergeMatchError(converge_id, "汇聚网关只能汇聚来自同一个并行网关的分支")
|
||||
|
||||
shared = True
|
||||
|
||||
elif converge_incoming < gateway_outgoing:
|
||||
raise exceptions.ConvergeMatchError(converge_id, "汇聚网关没有汇聚其对应的并行网关的所有分支")
|
||||
|
||||
current_gateway["match"] = converge_id
|
||||
current_gateway["share_converge"] = shared
|
||||
current_gateway["converge_end"] = converge_end
|
||||
|
||||
converged.setdefault(converge_id, []).append(current_gateway["id"])
|
||||
block_nodes[block_start].add(current_gateway["id"])
|
||||
|
||||
return converge_id, shared
|
||||
|
||||
|
||||
def distance_from(origin, node, tree, marked, visited=None):
|
||||
"""
|
||||
get max distance from origin to node
|
||||
:param origin:
|
||||
:param node:
|
||||
:param tree:
|
||||
:param marked:
|
||||
:param visited:
|
||||
:return:
|
||||
"""
|
||||
if visited is None:
|
||||
visited = set()
|
||||
|
||||
if node["id"] in marked:
|
||||
return marked[node["id"]]
|
||||
|
||||
if node["id"] == origin["id"]:
|
||||
return 0
|
||||
|
||||
if node["id"] in visited:
|
||||
# do not trace circle
|
||||
return None
|
||||
|
||||
visited.add(node["id"])
|
||||
|
||||
incoming_dist = []
|
||||
for incoming in node["incoming"]:
|
||||
prev_node = get_node_for_sequence(incoming, tree, "source")
|
||||
|
||||
# get incoming node's distance recursively
|
||||
dist = distance_from(origin=origin, node=prev_node, tree=tree, marked=marked, visited=visited)
|
||||
|
||||
# if this incoming do not trace back to current node
|
||||
if dist is not None:
|
||||
incoming_dist.append(dist + 1)
|
||||
|
||||
if not incoming_dist:
|
||||
return None
|
||||
|
||||
# get max distance
|
||||
res = max(incoming_dist)
|
||||
marked[node["id"]] = res
|
||||
return res
|
||||
|
||||
|
||||
def validate_gateways(tree):
|
||||
"""
|
||||
check sanity of gateways and find their converge gateway
|
||||
:param tree:
|
||||
:return:
|
||||
"""
|
||||
converges = {}
|
||||
gateways = {}
|
||||
all = {}
|
||||
distances = {}
|
||||
converge_positive_in = {}
|
||||
process_order = []
|
||||
|
||||
# data preparation
|
||||
for i, item in list(tree["gateways"].items()):
|
||||
node = {
|
||||
"incoming": item["incoming"] if isinstance(item["incoming"], list) else [item["incoming"]],
|
||||
"outgoing": item["outgoing"] if isinstance(item["outgoing"], list) else [item["outgoing"]],
|
||||
"type": item["type"],
|
||||
"target": [],
|
||||
"source": [],
|
||||
"id": item["id"],
|
||||
"match": None,
|
||||
}
|
||||
|
||||
# find all first reach nodes(ConvergeGateway, ExclusiveGateway, ParallelGateway, EndEvent)
|
||||
# which is not ServiceActivity for each gateway
|
||||
for index in node["outgoing"]:
|
||||
index = tree["flows"][index]["target"]
|
||||
while index in tree["activities"]:
|
||||
index = tree["flows"][tree["activities"][index]["outgoing"]]["target"]
|
||||
|
||||
# append this node's id to current gateway's target list
|
||||
node["target"].append(index)
|
||||
|
||||
# get current node's distance from start event
|
||||
if not distance_from(node=node, origin=tree["start_event"], tree=tree, marked=distances):
|
||||
raise exceptions.ConvergeMatchError(node["id"], "无法获取该网关距离开始节点的距离")
|
||||
|
||||
if item["type"] == "ConvergeGateway":
|
||||
converges[i] = node
|
||||
else:
|
||||
process_order.append(i)
|
||||
gateways[i] = node
|
||||
|
||||
all[i] = node
|
||||
|
||||
# calculate positive incoming number for converge
|
||||
for nid, node in list(all.items()):
|
||||
for t in node["target"]:
|
||||
if t in converges and distances[t] > distances[nid]:
|
||||
converge_positive_in[t] = converge_positive_in.setdefault(t, 0) + 1
|
||||
|
||||
process_order.sort(key=lambda gid: distances[gid])
|
||||
end_event_id = tree["end_event"]["id"]
|
||||
converged = {}
|
||||
block_nodes = {}
|
||||
visited = set()
|
||||
|
||||
# process in distance order
|
||||
for gw in process_order:
|
||||
if gw in visited or "match" in gw:
|
||||
continue
|
||||
visited.add(gw)
|
||||
|
||||
block_nodes[gw] = set()
|
||||
|
||||
match_converge(
|
||||
converges=converges,
|
||||
gateways=gateways,
|
||||
cur_index=gw,
|
||||
end_event_id=end_event_id,
|
||||
converged=converged,
|
||||
block_start=gw,
|
||||
block_nodes=block_nodes,
|
||||
dist_from_start=distances,
|
||||
converge_in_len=converge_positive_in,
|
||||
)
|
||||
|
||||
# set converge gateway
|
||||
for i in gateways:
|
||||
if gateways[i]["match"]:
|
||||
tree["gateways"][i]["converge_gateway_id"] = gateways[i]["match"]
|
||||
|
||||
return converged
|
||||
|
||||
|
||||
def blend(source, target, custom_stream=None):
|
||||
"""
|
||||
blend source and target streams
|
||||
:param source:
|
||||
:param target:
|
||||
:param custom_stream:
|
||||
:return:
|
||||
"""
|
||||
|
||||
if custom_stream:
|
||||
# use custom stream instead of source's stream
|
||||
if isinstance(custom_stream, set):
|
||||
for stream in custom_stream:
|
||||
target[STREAM].add(stream)
|
||||
else:
|
||||
target[STREAM].add(custom_stream)
|
||||
|
||||
return
|
||||
|
||||
if len(source[STREAM]) == 0:
|
||||
raise exceptions.InvalidOperationException("stream validation error, node(%s) stream is empty" % source["id"])
|
||||
|
||||
# blend
|
||||
for s in source[STREAM]:
|
||||
target[STREAM].add(s)
|
||||
|
||||
|
||||
def streams_for_parallel(p):
|
||||
streams = set()
|
||||
for i, target_id in enumerate(p["target"]):
|
||||
streams.add("{}_{}".format(p["id"], i))
|
||||
|
||||
return streams
|
||||
|
||||
|
||||
def flowing(where, to, parallel_converges):
|
||||
"""
|
||||
mark target's stream from target
|
||||
:param where:
|
||||
:param to:
|
||||
:param parallel_converges:
|
||||
:return:
|
||||
"""
|
||||
is_parallel = where["type"] in PARALLEL_GATEWAYS
|
||||
|
||||
stream = None
|
||||
if is_parallel:
|
||||
# add parallel's stream to its converge
|
||||
parallel_converge = to[where["converge_gateway_id"]]
|
||||
blend(source=where, target=parallel_converge, custom_stream=stream)
|
||||
|
||||
if len(parallel_converge[STREAM]) > 1:
|
||||
raise exceptions.StreamValidateError(node_id=parallel_converge)
|
||||
|
||||
# flow to target
|
||||
for i, target_id in enumerate(where["target"]):
|
||||
target = to[target_id]
|
||||
fake = False
|
||||
|
||||
# generate different stream
|
||||
if is_parallel:
|
||||
stream = "{}_{}".format(where["id"], i)
|
||||
|
||||
if target_id in parallel_converges:
|
||||
|
||||
is_valid_branch = where[STREAM].issubset(parallel_converges[target_id][P_STREAM])
|
||||
is_direct_connect = where.get("converge_gateway_id") == target_id
|
||||
|
||||
if is_valid_branch or is_direct_connect:
|
||||
# do not flow when branch of parallel converge to its converge gateway
|
||||
fake = True
|
||||
|
||||
if not fake:
|
||||
blend(source=where, target=target, custom_stream=stream)
|
||||
|
||||
# sanity check
|
||||
if len(target[STREAM]) != 1:
|
||||
raise exceptions.StreamValidateError(node_id=target_id)
|
||||
|
||||
|
||||
def validate_stream(tree):
|
||||
"""
|
||||
validate flow stream
|
||||
:param tree: pipeline tree
|
||||
:return:
|
||||
"""
|
||||
# data preparation
|
||||
start_event_id = tree["start_event"]["id"]
|
||||
end_event_id = tree["end_event"]["id"]
|
||||
nodes = get_nodes_dict(tree)
|
||||
nodes[start_event_id][STREAM] = {MAIN_STREAM}
|
||||
nodes[end_event_id][STREAM] = {MAIN_STREAM}
|
||||
parallel_converges = {}
|
||||
visited = set({})
|
||||
|
||||
for nid, node in list(nodes.items()):
|
||||
node.setdefault(STREAM, set())
|
||||
|
||||
# set allow streams for parallel's converge
|
||||
if node["type"] in PARALLEL_GATEWAYS:
|
||||
parallel_converges[node["converge_gateway_id"]] = {
|
||||
P_STREAM: streams_for_parallel(node),
|
||||
P: nid,
|
||||
}
|
||||
|
||||
# build stream from start
|
||||
node_queue = queue.Queue()
|
||||
node_queue.put(nodes[start_event_id])
|
||||
while not node_queue.empty():
|
||||
|
||||
# get node
|
||||
node = node_queue.get()
|
||||
|
||||
if node["id"] in visited:
|
||||
# flow again to validate stream, but do not add target to queue
|
||||
flowing(where=node, to=nodes, parallel_converges=parallel_converges)
|
||||
continue
|
||||
|
||||
# add to queue
|
||||
for target_id in node["target"]:
|
||||
node_queue.put(nodes[target_id])
|
||||
|
||||
# mark as visited
|
||||
visited.add(node["id"])
|
||||
|
||||
# flow
|
||||
flowing(where=node, to=nodes, parallel_converges=parallel_converges)
|
||||
|
||||
# data clean
|
||||
for nid, n in list(nodes.items()):
|
||||
if len(n[STREAM]) != 1:
|
||||
raise exceptions.StreamValidateError(node_id=nid)
|
||||
|
||||
# replace set to str
|
||||
n[STREAM] = n[STREAM].pop()
|
||||
|
||||
# isolate node check
|
||||
for __, node in list(nodes.items()):
|
||||
if not node[STREAM]:
|
||||
raise exceptions.IsolateNodeError()
|
||||
|
||||
return nodes
|
||||
87
lib/bamboo_engine/validator/rules.py
Normal file
87
lib/bamboo_engine/validator/rules.py
Normal file
@@ -0,0 +1,87 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS Community
|
||||
Edition) available.
|
||||
Copyright (C) 2017-2021 THL A29 Limited, a Tencent company. All rights reserved.
|
||||
Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
http://opensource.org/licenses/MIT
|
||||
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.
|
||||
"""
|
||||
|
||||
from bamboo_engine.eri import NodeType
|
||||
|
||||
MAX_IN = 1000
|
||||
MAX_OUT = 1000
|
||||
FLOW_NODES_WITHOUT_STARTEVENT = [
|
||||
NodeType.ServiceActivity.value,
|
||||
NodeType.SubProcess.value,
|
||||
NodeType.EmptyEndEvent.value,
|
||||
NodeType.ParallelGateway.value,
|
||||
NodeType.ConditionalParallelGateway.value,
|
||||
NodeType.ExclusiveGateway.value,
|
||||
NodeType.ConvergeGateway.value,
|
||||
]
|
||||
|
||||
FLOW_NODES_WITHOUT_START_AND_END = [
|
||||
NodeType.ServiceActivity.value,
|
||||
NodeType.SubProcess.value,
|
||||
NodeType.ParallelGateway.value,
|
||||
NodeType.ConditionalParallelGateway.value,
|
||||
NodeType.ExclusiveGateway.value,
|
||||
NodeType.ConvergeGateway.value,
|
||||
]
|
||||
|
||||
SOURCE_RULE = {
|
||||
"min_in": 0,
|
||||
"max_in": 0,
|
||||
"min_out": 1,
|
||||
"max_out": 1,
|
||||
"allowed_out": FLOW_NODES_WITHOUT_START_AND_END,
|
||||
}
|
||||
|
||||
SINK_RULE = {
|
||||
"min_in": 1,
|
||||
"max_in": MAX_IN,
|
||||
"min_out": 0,
|
||||
"max_out": 0,
|
||||
"allowed_out": [],
|
||||
}
|
||||
|
||||
ACTIVITY_RULE = {
|
||||
"min_in": 1,
|
||||
"max_in": MAX_IN,
|
||||
"min_out": 1,
|
||||
"max_out": 1,
|
||||
"allowed_out": FLOW_NODES_WITHOUT_STARTEVENT,
|
||||
}
|
||||
|
||||
EMIT_RULE = {
|
||||
"min_in": 1,
|
||||
"max_in": MAX_IN,
|
||||
"min_out": 1,
|
||||
"max_out": MAX_OUT,
|
||||
"allowed_out": FLOW_NODES_WITHOUT_STARTEVENT,
|
||||
}
|
||||
|
||||
CONVERGE_RULE = {
|
||||
"min_in": 1,
|
||||
"max_in": MAX_IN,
|
||||
"min_out": 1,
|
||||
"max_out": 1,
|
||||
"allowed_out": FLOW_NODES_WITHOUT_STARTEVENT,
|
||||
}
|
||||
|
||||
# rules of activity graph
|
||||
NODE_RULES = {
|
||||
NodeType.EmptyStartEvent.value: SOURCE_RULE,
|
||||
NodeType.EmptyEndEvent.value: SINK_RULE,
|
||||
NodeType.ServiceActivity.value: ACTIVITY_RULE,
|
||||
NodeType.ExclusiveGateway.value: EMIT_RULE,
|
||||
NodeType.ParallelGateway.value: EMIT_RULE,
|
||||
NodeType.ConditionalParallelGateway.value: EMIT_RULE,
|
||||
NodeType.ConvergeGateway.value: CONVERGE_RULE,
|
||||
NodeType.SubProcess.value: ACTIVITY_RULE,
|
||||
}
|
||||
90
lib/bamboo_engine/validator/utils.py
Normal file
90
lib/bamboo_engine/validator/utils.py
Normal file
@@ -0,0 +1,90 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS Community
|
||||
Edition) available.
|
||||
Copyright (C) 2017-2021 THL A29 Limited, a Tencent company. All rights reserved.
|
||||
Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
http://opensource.org/licenses/MIT
|
||||
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.
|
||||
"""
|
||||
|
||||
from copy import deepcopy
|
||||
|
||||
from bamboo_engine.exceptions import ValueError
|
||||
|
||||
|
||||
def format_to_list(notype):
|
||||
"""
|
||||
format a data to list
|
||||
:return:
|
||||
"""
|
||||
if isinstance(notype, list):
|
||||
return notype
|
||||
if not notype:
|
||||
return []
|
||||
return [notype]
|
||||
|
||||
|
||||
def format_node_io_to_list(node, i=True, o=True):
|
||||
if i:
|
||||
node["incoming"] = format_to_list(node["incoming"])
|
||||
|
||||
if o:
|
||||
node["outgoing"] = format_to_list(node["outgoing"])
|
||||
|
||||
|
||||
def format_pipeline_tree_io_to_list(pipeline_tree):
|
||||
"""
|
||||
:summary: format incoming and outgoing to list
|
||||
:param pipeline_tree:
|
||||
:return:
|
||||
"""
|
||||
for act in list(pipeline_tree["activities"].values()):
|
||||
format_node_io_to_list(act, o=False)
|
||||
|
||||
for gateway in list(pipeline_tree["gateways"].values()):
|
||||
format_node_io_to_list(gateway, o=False)
|
||||
|
||||
format_node_io_to_list(pipeline_tree["end_event"], o=False)
|
||||
|
||||
|
||||
def get_node_for_sequence(sid, tree, node_type):
|
||||
target_id = tree["flows"][sid][node_type]
|
||||
|
||||
if target_id in tree["activities"]:
|
||||
return tree["activities"][target_id]
|
||||
elif target_id in tree["gateways"]:
|
||||
return tree["gateways"][target_id]
|
||||
elif target_id == tree["end_event"]["id"]:
|
||||
return tree["end_event"]
|
||||
elif target_id == tree["start_event"]["id"]:
|
||||
return tree["start_event"]
|
||||
|
||||
raise ValueError("node(%s) not in data" % target_id)
|
||||
|
||||
|
||||
def get_nodes_dict(data):
|
||||
"""
|
||||
get all FlowNodes of a pipeline
|
||||
"""
|
||||
data = deepcopy(data)
|
||||
start = data["start_event"]["id"]
|
||||
end = data["end_event"]["id"]
|
||||
|
||||
nodes = {start: data["start_event"], end: data["end_event"]}
|
||||
|
||||
nodes.update(data["activities"])
|
||||
nodes.update(data["gateways"])
|
||||
|
||||
for node in list(nodes.values()):
|
||||
# format to list
|
||||
node["incoming"] = format_to_list(node["incoming"])
|
||||
node["outgoing"] = format_to_list(node["outgoing"])
|
||||
|
||||
node["source"] = [data["flows"][incoming]["source"] for incoming in node["incoming"]]
|
||||
node["target"] = [data["flows"][outgoing]["target"] for outgoing in node["outgoing"]]
|
||||
|
||||
return nodes
|
||||
16
lib/pipeline/__init__.py
Normal file
16
lib/pipeline/__init__.py
Normal file
@@ -0,0 +1,16 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS Community
|
||||
Edition) available.
|
||||
Copyright (C) 2017-2021 THL A29 Limited, a Tencent company. All rights reserved.
|
||||
Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
http://opensource.org/licenses/MIT
|
||||
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.
|
||||
"""
|
||||
|
||||
default_app_config = "pipeline.apps.PipelineConfig"
|
||||
|
||||
__version__ = "3.14.1"
|
||||
32
lib/pipeline/admin.py
Normal file
32
lib/pipeline/admin.py
Normal file
@@ -0,0 +1,32 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS Community
|
||||
Edition) available.
|
||||
Copyright (C) 2017-2021 THL A29 Limited, a Tencent company. All rights reserved.
|
||||
Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
http://opensource.org/licenses/MIT
|
||||
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.
|
||||
"""
|
||||
|
||||
from django.contrib import admin
|
||||
|
||||
from pipeline import models
|
||||
|
||||
|
||||
@admin.register(models.PipelineTemplate)
|
||||
class PipelineTemplateAdmin(admin.ModelAdmin):
|
||||
list_display = ["id", "template_id", "name", "create_time", "edit_time"]
|
||||
list_filter = ["is_deleted"]
|
||||
search_fields = ["name"]
|
||||
raw_id_fields = ["snapshot"]
|
||||
|
||||
|
||||
@admin.register(models.PipelineInstance)
|
||||
class PipelineInstanceAdmin(admin.ModelAdmin):
|
||||
list_display = ["id", "template", "name", "instance_id", "create_time", "start_time", "finish_time", "is_deleted"]
|
||||
list_filter = ["is_started", "is_finished", "is_revoked", "is_deleted"]
|
||||
search_fields = ["name"]
|
||||
raw_id_fields = ["template", "snapshot", "execution_snapshot", "tree_info"]
|
||||
100
lib/pipeline/apps.py
Normal file
100
lib/pipeline/apps.py
Normal file
@@ -0,0 +1,100 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS Community
|
||||
Edition) available.
|
||||
Copyright (C) 2017-2021 THL A29 Limited, a Tencent company. All rights reserved.
|
||||
Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
http://opensource.org/licenses/MIT
|
||||
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 sys
|
||||
import logging
|
||||
import traceback
|
||||
|
||||
import redis
|
||||
from django.apps import AppConfig
|
||||
from django.conf import settings
|
||||
from redis.sentinel import Sentinel
|
||||
from rediscluster import RedisCluster
|
||||
|
||||
logger = logging.getLogger("root")
|
||||
|
||||
|
||||
def get_client_through_sentinel():
|
||||
kwargs = {"sentinel_kwargs": {}}
|
||||
sentinel_pwd = settings.REDIS.get("sentinel_password")
|
||||
if sentinel_pwd:
|
||||
kwargs["sentinel_kwargs"]["password"] = sentinel_pwd
|
||||
if "password" in settings.REDIS:
|
||||
kwargs["password"] = settings.REDIS["password"]
|
||||
host = settings.REDIS["host"]
|
||||
port = settings.REDIS["port"]
|
||||
sentinels = list(zip([h.strip() for h in host.split(",")], [p.strip() for p in str(port).split(",")],))
|
||||
rs = Sentinel(sentinels, **kwargs)
|
||||
# avoid None value in settings.REDIS
|
||||
r = rs.master_for(settings.REDIS.get("service_name") or "mymaster")
|
||||
# try to connect master
|
||||
r.echo("Hello Redis")
|
||||
return r
|
||||
|
||||
|
||||
def get_cluster_client():
|
||||
kwargs = {"startup_nodes": [{"host": settings.REDIS["host"], "port": settings.REDIS["port"]}]}
|
||||
if "password" in settings.REDIS:
|
||||
kwargs["password"] = settings.REDIS["password"]
|
||||
|
||||
r = RedisCluster(**kwargs)
|
||||
r.echo("Hello Redis")
|
||||
return r
|
||||
|
||||
|
||||
def get_single_client():
|
||||
kwargs = {
|
||||
"host": settings.REDIS["host"],
|
||||
"port": settings.REDIS["port"],
|
||||
}
|
||||
if "password" in settings.REDIS:
|
||||
kwargs["password"] = settings.REDIS["password"]
|
||||
if "db" in settings.REDIS:
|
||||
kwargs["db"] = settings.REDIS["db"]
|
||||
|
||||
pool = redis.ConnectionPool(**kwargs)
|
||||
return redis.StrictRedis(connection_pool=pool)
|
||||
|
||||
|
||||
CLIENT_GETTER = {
|
||||
"replication": get_client_through_sentinel,
|
||||
"cluster": get_cluster_client,
|
||||
"single": get_single_client,
|
||||
}
|
||||
|
||||
|
||||
class PipelineConfig(AppConfig):
|
||||
name = "pipeline"
|
||||
verbose_name = "Pipeline"
|
||||
|
||||
def ready(self):
|
||||
from pipeline.signals.handlers import pipeline_template_post_save_handler # noqa
|
||||
from pipeline.validators.handlers import post_new_end_event_register_handler # noqa
|
||||
|
||||
# init redis pool
|
||||
if hasattr(settings, "REDIS"):
|
||||
mode = settings.REDIS.get("mode") or "single"
|
||||
try:
|
||||
settings.REDIS_INST = CLIENT_GETTER[mode]()
|
||||
settings.redis_inst = CLIENT_GETTER[mode]()
|
||||
except Exception:
|
||||
# fall back to single node mode
|
||||
logger.error("redis client init error: %s" % traceback.format_exc())
|
||||
elif (
|
||||
getattr(settings, "PIPELINE_DATA_BACKEND", None)
|
||||
== "pipeline.engine.core.data.redis_backend.RedisDataBackend"
|
||||
):
|
||||
logger.error("can not find REDIS in settings!")
|
||||
|
||||
# avoid big flow pickle raise maximum recursion depth exceeded error
|
||||
sys.setrecursionlimit(10000)
|
||||
15
lib/pipeline/builder/__init__.py
Normal file
15
lib/pipeline/builder/__init__.py
Normal file
@@ -0,0 +1,15 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS Community
|
||||
Edition) available.
|
||||
Copyright (C) 2017-2021 THL A29 Limited, a Tencent company. All rights reserved.
|
||||
Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
http://opensource.org/licenses/MIT
|
||||
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.
|
||||
"""
|
||||
|
||||
from .builder import * # noqa
|
||||
from .flow import * # noqa
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user