Refer to About standard plugins
Execute python manage.py create_atoms_app {CUSTOM PLUGINS NAME}
under project root directory, where {CUSTOM PLUGINS NAME}
is the standard plugin pack name you are developing.
Please be careful not to use the same name with existing modules and plugin packs. The name should reflect the function of the plugin pack. After command execution, the following directory structure will be generated:
{CUSTOM PLUGINS NAME}
├── __init__.py
├── apps.py
├── components
│ ├── __init__.py
│ └── collections
│ ├── __init__.py
│ └── plugins.py
├── migrations
│ └── __init__.py
├── static
│ └── {CUSTOM PLUGINS NAME}
│ └── plugins.js
└── tests
├── __init__.py
└── components
├── __init__.py
└── collections
├── __init__.py
└── plugins_test
└── __init__.py
In the above directory structure, standard plugin pack background code files are placed in components
directory, standard plugin pack frontend static code files are placed in static
directory. plugin.py
and plugin.js
can be renamed according to
the system name abbreviation you are developing the standard plugin for, such as job, cmdb, etc.
Open config/default.py
, find the INSTALLED_APPS variable, and add the {CUSTOM PLUGINS NAME}
you created in step 1.
If the standard plugin you are developing uses a custom API gateway, after you connect the interface to the Blueking API gateway, you need to manually add the API to the Client SDK.
Write the following code in the {CUSTOM PLUGINS NAME}/__init__.py
file to add the corresponding interface to the Client:
from packages.blueking.component import collections
from packages.blueking.component.base import ComponentAPI
from packages.blueking.component.client import ComponentClient
class CollectionsMySystem(object):
def __init__(self, client):
self.client = client
self.get_status = ComponentAPI(
client=self.client, method='GET',
path='/api/c/compapi{bk_api_ver}/my_system/status/',
description=u"Get Status"
)
self.set_status = ComponentAPI(
client=self.client, method='POST',
path='/api/c/compapi{bk_api_ver}/my_system/status/',
description=u"Set Status"
)
collections.AVAILABLE_COLLECTIONS.update({
'my_system': CollectionsMySystem
})
ComponentClient.setup_components(collections.AVAILABLE_COLLECTIONS)
In the above example, a system named my_system
is added to the Client, and two interfaces, get_status
and set_status
, are added to the system.
Execute the command in the project root directory
python manage.py create_new_plugin {group_code} {plugin_code} {version} {plugin_env} {app_code} {append}
where
- group_code is the series to which the plugin belongs (e.g. cc, tgw, gcs)
- plugin_code is the code of the plugin (e.g. create_set)
- versionis the version of the plugin (e.g. v1.0)
- plugin_env is the type of plugin (e.g. open, ieod)
- app_code is the code of the app where the plugin is located (e.g. pipeline_plugins)
- append is the end of the directory where the plugin is located (e.g. the sites in pipeline_plugins/components/collections/sites. Can be empty)
eg.
python manage.py create_new_plugin cc create_set v1.0 open pipeline_plugins sites
In the above example, we are creating CC-series plugin called create_set. Its version number is v1.0 and is an open source plugin.
Write plugin background logic in plugin.py
. It mainly includes standard plugin definitions and background execution logic. A sample code is given below.
# -*- coding: utf-8 -*-
import logging
from django.utils.translation import ugettext_lazy as _
from pipeline.core.flow.activity import Service
from pipeline.component_framework.component import Component
from gcloud.conf import settings
logger = logging.getLogger('celery')
get_client_by_user = settings.ESB_GET_CLIENT_BY_USER
__group_name__ = _(u"CustomPlugin(CUSTOM)")
class TestCustomService(Service):
__need_schedule__ = False
def execute_pre_process(self, data, parent_data):
test_input = data.inputs.test_input
if not test_input.startswith("test_"):
message = "test_input should start with 'test_'"
data.set_outputs('ex_data', message)
return False
return True
def execute(self, data, parent_data):
executor = parent_data.inputs.executor
biz_cc_id = parent_data.inputs.biz_cc_id
client = get_client_by_user(executor)
test_input = data.inputs.test_input
test_textarea = data.inputs.test_textarea
test_radio = data.inputs.test_radio
api_kwargs = {
'biz_biz_id': biz_cc_id,
'executor': executor,
'test_input': test_input,
'test_textarea': test_textarea,
'test_radio': test_radio,
}
api_result = client.test_api.test1(api_kwargs)
logger.info('test_api result: {result}, api_kwargs: {kwargs}'.format(result=api_result, kwargs=api_kwargs))
if api_result['result']:
data.set_outputs('data1', api_result['data']['data1'])
return True
else:
data.set_outputs('ex_data', api_result['message'])
return False
def outputs_format(self):
return [
self.OutputItem(name=_(u'ResultData1'), key='data1', type='string')
]
class TestCustomComponent(Component):
name = _(u"CustomPluginTest")
code = 'test_custom'
bound_service = TestCustomService
form = '%scustom_plugins/plugin.js' % settings.STATIC_URL
version = '1.1.0'
The meaning of each attribute and class is:
__group_name__
: the group to which the standard plugin belongs (it usually corresponds to the system abbreviation corresponding to the API, such as CMDB).class TestCustomService(Service)
: standard plugin background execution logicclass TestCustomComponent(Component)
: standard plugin definition, frontend and backend service binding.
TestCustomService class explanation:
__need_schedule__
: whether it is an asynchronous standard plugin (including asynchronous polling and asynchronous callback). It is set to False by default.interval
: the polling policy of asynchronous standard plugindef execute_pre_process
: pre-processing before standard plugin execution. It can pre-process and validate plugin input data and return a True/False result. If the result is False, execute function will not be called.def execute
: Standard plugin execution logic, including frontend parameter acquisition, API parameter assembly, result analysis and result output.def schedule
: the polling or callback logic of asynchronous standard plugin. This method does not need to be defined when using synchronous standard plugin.def outputs_format
: output parameters definition.def inputs_format
: input parameters definition.
execute_pre_process function explanation:
- It can be any python code. It validates and pre-processes the plugin data and returns a pre-process result. The function is not necessary, and it returns True by default.
- data is the standard plugin parameter data input and output object. The input parameter corresponds to the form in the frontend. A parameter can be acquired by using
data.inputs.xxx
ordata.get_one_of_inputs('xxx')
; After execution,data.set_outputs
can be used to write output parameter. Exceptions should be assigned toex_data
. parent_data
is the public parameter of task. It includes executor (executor), operator (operator), biz_cc_id (business ID), etc. Refer togcloud/taskflow3/utils.py
for details.False
means the pre-process or validation failed. The failed execution result and exception will be returned, andexecute
andschedule
function will not be executed;True
means the pre-process or validation is successful andexecute
function will be executed normally.
execute function explanation:
- It can be any python code, if Blueking API gateway interface needs to be called. It consists of parameter assembly, API call, and result parsing.
- data is the standard plugin parameter data input and output object. The input parameter corresponds to the form in the frontend. A parameter can be acquired by using
data.inputs.xxx
ordata.get_one_of_inputs('xxx')
; After execution,data.set_outputs
can be used to write output parameter. Exceptions should be assigned toex_data
. parent_data
is the public parameter of task. It includes executor (executor), operator (operator), biz_cc_id (business ID), etc. Refer togcloud/taskflow3/utils.py
for details.False
means execution failed. When synchronous plugin returnsTrue
, it means standard plugin execution success. When asynchronous plugin returnsTrue
, it will go into hibernation and wait for the first asynchronous polling or external callback, then executeschedule
function.
outputs_format function explanation:
- Return the list of output parameters.
- Each item in the list format defines a returned field, which is a subset of the output field of
set_outputs
inexecute
function;key
is the output field identifier,name
is the output field definition,type
is the output field type (str
,int
and otherpython
data structure).
inputs_format function explanation:
- Return the list of input parameters.
- This method provides an explanation of the input parameter and does not affect code execution.
schedule function explanation:
- The scheduling strategy is controlled by the
interval
attribute of theTestCustomService
class, such aspipeline.core.flow.activity.StaticIntervalGenerator
(seconds between each polling),SquareIntervalGenerator
(The interval between each polling is the square of the current scheduled times). - Use
self.finish_schedule
to end polling.True
indicates that the standard plugin has been executed successfully.False
indicates execution failed.
TestCustomComponent class explanation:
name
: standard plugin name.code
: the unique ID of standard plugin, please keepcode
andversion
globally unique.bound_service
: bind backgound serviceTestCustomService
.form
: frontend form file path, please addsettings.STATIC_URL
prefix.version
: plugin version number string. Used for version control of plugins with the samecode
.
Sometimes, we need to update a certain plugin, such as adding new fields to the form, adding new functions to the background logic. For this, we need to modify the logic of the current plugin; If the current user flow and task are using this plugin, modifying the code of the plugin directly may cause the current flow and tasks to become unavailable, so the correct approach should be adding a new version of this plugin.
By configuring the version
class attribute of Component, we can set plugins with the same code
to different version. This will ensure that the plugin function update will not affect the user. The user simply needs to update the old plugin when necessary.
Note: please don't add the version
field to the plugins without version
parameter. The system may treat it as a new plugin, and templates and tasks may not work properly.
There are no restrictions on how the version field is written. However, it is best to use comprehensible nomenclature.
The frontend logic is written in plugin.js
file. Configure the standard operation frontend plugin framework to generate frontend form. A sample code is given below.
(function(){
$.atoms.test_custom = [
{
tag_code: "test_input",
type: "input",
attrs: {
name: gettext("Parameter 1"),
placeholder: gettext("Please enter string"),
hookable: true,
validation: [
{
type: "required"
}
]
}
},
{
tag_code: "test_textarea",
type: "textarea",
attrs: {
name: gettext("Parameter 2"),
placeholder: gettext("separated by line break"),
hookable: true,
validation: [
{
type: "required"
}
]
}
},
{
tag_code: "test_radio",
type: "radio",
attrs: {
name: gettext("Parameter 3"),
items: [
{value: "1", name: gettext("Option 1")},
{value: "2", name: gettext("Option 2")},
{value: "3", name: gettext("Option 3")}
],
default: "1",
hookable: true,
validation: [
{
type: "required"
}
]
}
}
]
})();
Standard plugin frontend configuration is registered through $.atoms. The various items are:
test_custom
: the code defined in the standard plugin backgroundtag_code
: parameter code. Please keep this one globally unique, the naming convention is "SystemName_ParameterName".type
: frontend form type. The options are: input, textarea, radio, checkbox, select, datetime, datatable, upload, combine,etc.attrs
: Attribute settings for the corresponding type, such as name, validation, etc.
In addition, form items from another standard plugin can be inherited by the standard plugin frontend configuration. The following attributes need to be defined:
extend
: Inherit the form items from another standard plugin. Format:Base.TagA.TagB...
,Base
is the name of other plugins.Tag
is the value oftag_code
of plugin form items. If only theBase
value is define, all form items in that standard plugin will be inherited.config
: overwrites the inherited standard plugin form item configuration. It is not a required attribute. The data type should be identical to the inherited configuration item data type. If the inherited configuration is an object, then config object's tag_code needs to be set to the tag_code value of the inherited object. The attributes of two objects should be added together. When there is a same attribute, the config object attribute value will replace the inherited object attribute value; If the inherited configuration is an array. For example, in a scenario where onlyBase
is defined, config needs to be set to array. Form configuration elements in the array will be merged with the inherited configuration. The overwrite rules for configuration items with the same tag_code remains the same.
An example of standard plugin frontend inheritance:
// base.js
(function () {
$.atoms.base_custom = [
{
tag_code: "test_input",
type: "input",
attrs: {
name: gettext("Parameter 1"),
placeholder: gettext("Please enter string"),
hookable: true,
validation: [
{
type: "required"
}
]
}
}
]
})()
// test_custom.js
(function () {
$.atoms.test_custom = [
{
extend: "base.test_input",
config: {
tag_code: "test_input",
type: "textarea"
}
},
{
tag_code: "test_radio",
type: "radio",
attrs: {
name: gettext("Parameter 3"),
items: [
{value: "1", name: gettext("Option 1")},
{value: "2", name: gettext("Option 2")},
{value: "3", name: gettext("Option 3")}
],
default: "1",
hookable: true,
validation: [
{
type: "required"
}
]
}
}
]
})()
// The test_custom.js after successful inheritance
(function () {
$.atoms.test_custom = [
{
tag_code: "test_input",
type: "text", // type attribute is overwritten
attrs: {
name: gettext("Parameter 1"),
placeholder: gettext("Please enter string"),
hookable: true,
validation: [
{
type: "required"
}
]
}
}
{
tag_code: "test_radio",
type: "radio",
attrs: {
name: gettext("Parameter 3"),
items: [
{value: "1", name: gettext("Option 1")},
{value: "2", name: gettext("Option 2")},
{value: "3", name: gettext("Option 3")}
],
default: "1",
hookable: true,
validation: [
{
type: "required"
}
]
}
}
]
})()
For details on field descriptions, please refer to: Tag Usage and Development.
Standard plugins outside the system can be loaded in SOPS. However, these plugins have to meet the following requirements (If your plugin does not need to be loaded remotely, you can skip this chapter):
- It needs to declare the third-party python component library that does not exist in the system
- The embedding method used by the plugin form
If your plugin requires a third-party python plugin that does not exist when SOPS is running, you need to
add __requirements__
variable in {CUSTOM PLUGINS NAME}/__init__.py
and declare the name and version number of the component:
# {CUSTOM PLUGINS NAME}/__init__.py
__requirements__ = [
"flask==x.x.x", # version restriction
"mako", # no version restriction
]
Remotely loaded plugins currently do not support reading plugin frontend forms from static files, so the form should be added to the background definition of the plugin by embedding:
- Add
embedded_form
attribute to Component class and set the value toTrue
- Add
form
attribute to the Component class and set its value to form definition
The following example shows how to declare plugin embedded form defined in section 4 and 5:
class TestCustomComponent(Component):
name = _(u"CustomPluginTest")
code = 'test_custom'
bound_service = TestCustomService
embedded_form = True # Embedded form declaration
# Define form
form = """
(function(){
$.atoms.test_custom = [
{
tag_code: "test_input",
type: "input",
attrs: {
name: gettext("Parameter 1"),
placeholder: gettext("Please enter string"),
hookable: true,
validation: [
{
type: "required"
}
]
}
},
{
tag_code: "test_textarea",
type: "textarea",
attrs: {
name: gettext("Parameter 2"),
placeholder: gettext("separated by line break"),
hookable: true,
validation: [
{
type: "required"
}
]
}
},
{
tag_code: "test_radio",
type: "radio",
attrs: {
name: gettext("Parameter 3"),
items: [
{value: "1", name: gettext("Option 1")},
{value: "2", name: gettext("Option 2")},
{value: "3", name: gettext("Option 3")}
],
default: "1",
hookable: true,
validation: [
{
type: "required"
}
]
}
}
]
})();
"""
After we complete developing a custom component, we need to test that the component works as intended. The easiest way to do this is to create a workflow that contains the node and run the workflow to see if the node behaves and generates output as intended. However, this is a very time-consuming and one-time test. We have to run the same test again if the node is modified.
To solve this problem, a component unit testing framework is provided within the framework. The testing framework will simulate an environment for the component to run in and use test cases written by the developer to check whether the behavior of the component meets expectations. Thanks to the component unit testing framework, we can spend less time on testing. It also enables us to check whether changes will affect the functionality of the component.
Unit test for standard plugin requires a test file to be created in the same subdirectory path as the plugin definition file in {CUSTOM PLUGINS NAME}/tests
. For example,
for plugins in {CUSTOM PLUGINS NAME}/components/collections/plugins.py
,
you should create the corresponding testing files for each plugin and create unit testing under {CUSTOM PLUGINS NAME}/tests/components/collections/plugins_test
.
Additionally, the test file name should be test_{code}.py
, whereas {code}
is the unique ID of the plugin.
For instructions on writing unit tests, please refer to: Writing standard plugin unit test.
After a plugin is developed, you should execute python manage.py collectstatic –noinput
at root directory to collect static resources.
Then create a workflow template and add standard plugin node. Select new standard plugin as standard plugin type and make sure that the displayed input parameter is identical to frontend configuration and the output parameter is identical to backend outputs_format.
The execution result is system default. The True
/False
value indicates whether the node has been executed successfully.
Then use the created workflow to create a task, fill in the parameters and execute. After execution, check whether the result meets expectation. Use logs to evaluate the result more accurately.
- The naming convention for groups is "System name(System Abbreviation)". Example: Job Platform(JOB).
- Use underscore in standard plugin code name: "SystemName_InterfaceName". Example: job_execute_task.
- Use camel case for backend class names: "StandardPluginCode+ClassName". Example: JobExecuteTaskService.
- The file directory name of frontend JS should be identical to system abbreviation. The file name of JS should be identical to standard plugin code.
- The naming convention for tag_code parameter is "SystemName_ParameterName". This is to ensure that the name is globally unique; The length should not exceed 20 characters.
- Please apply translation function to Chinese characters in backend and frontend for future internalization and localization.